From 3666ec100f983e179c9d1fcba311b5e2f3a87d5f Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Mon, 2 Feb 2026 17:08:31 +0100 Subject: [PATCH 1/8] feat: Add InfluxDB support Signed-off-by: Pascal Zimmermann --- influxdb/cue.mod/module.cue | 13 ++ influxdb/go.mod | 36 ++++ influxdb/go.sum | 68 ++++++ influxdb/jest.config.ts | 7 + influxdb/package.json | 69 ++++++ influxdb/rsbuild.config.ts | 25 +++ .../examples/influxdb-v1-direct.yaml | 11 + .../examples/influxdb-v1-proxy.yaml | 22 ++ .../examples/influxdb-v3-direct.yaml | 12 ++ .../examples/influxdb-v3-proxy.yaml | 21 ++ .../influx-db-v1-datasource.cue | 34 +++ .../influx-db-v1-datasource.json | 8 + .../influx-db-v3-datasource.cue | 35 +++ .../influx-db-v3-datasource.json | 9 + influxdb/sdk/go/datasource/datasource.go | 158 ++++++++++++++ influxdb/sdk/go/datasource/datasource_test.go | 201 ++++++++++++++++++ influxdb/sdk/go/datasource/options.go | 66 ++++++ .../influxdb-v1/InfluxDBV1Datasource.ts | 43 ++++ .../influxdb-v1/InfluxDBV1Editor.tsx | 38 ++++ influxdb/src/datasources/influxdb-v1/index.ts | 15 ++ .../influxdb-v3/InfluxDBV3Datasource.ts | 47 ++++ .../influxdb-v3/InfluxDBV3Editor.tsx | 47 ++++ influxdb/src/datasources/influxdb-v3/index.ts | 15 ++ influxdb/src/getPluginModule.ts | 11 + influxdb/src/index.ts | 4 + influxdb/src/model/index.ts | 13 ++ influxdb/src/model/influxdb-types.ts | 59 +++++ .../InfluxDBTimeSeriesQuery.ts | 85 ++++++++ .../InfluxDBTimeSeriesQueryEditor.tsx | 32 +++ .../influxdb-time-series-query/index.ts | 15 ++ influxdb/tsconfig.build.json | 8 + influxdb/tsconfig.json | 3 + 32 files changed, 1230 insertions(+) create mode 100644 influxdb/cue.mod/module.cue create mode 100644 influxdb/go.mod create mode 100644 influxdb/go.sum create mode 100644 influxdb/jest.config.ts create mode 100644 influxdb/package.json create mode 100644 influxdb/rsbuild.config.ts create mode 100644 influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-direct.yaml create mode 100644 influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-proxy.yaml create mode 100644 influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-direct.yaml create mode 100644 influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-proxy.yaml create mode 100644 influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.cue create mode 100644 influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.json create mode 100644 influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.cue create mode 100644 influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.json create mode 100644 influxdb/sdk/go/datasource/datasource.go create mode 100644 influxdb/sdk/go/datasource/datasource_test.go create mode 100644 influxdb/sdk/go/datasource/options.go create mode 100644 influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts create mode 100644 influxdb/src/datasources/influxdb-v1/InfluxDBV1Editor.tsx create mode 100644 influxdb/src/datasources/influxdb-v1/index.ts create mode 100644 influxdb/src/datasources/influxdb-v3/InfluxDBV3Datasource.ts create mode 100644 influxdb/src/datasources/influxdb-v3/InfluxDBV3Editor.tsx create mode 100644 influxdb/src/datasources/influxdb-v3/index.ts create mode 100644 influxdb/src/getPluginModule.ts create mode 100644 influxdb/src/index.ts create mode 100644 influxdb/src/model/index.ts create mode 100644 influxdb/src/model/influxdb-types.ts create mode 100644 influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts create mode 100644 influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQueryEditor.tsx create mode 100644 influxdb/src/queries/influxdb-time-series-query/index.ts create mode 100644 influxdb/tsconfig.build.json create mode 100644 influxdb/tsconfig.json diff --git a/influxdb/cue.mod/module.cue b/influxdb/cue.mod/module.cue new file mode 100644 index 000000000..b5be31c96 --- /dev/null +++ b/influxdb/cue.mod/module.cue @@ -0,0 +1,13 @@ +module: "github.com/perses/plugins/influxdb@v0" +language: { + version: "v0.8.0" +} +source: { + kind: "git" +} +deps: { + "github.com/perses/perses/cue@v0": { + v: "v0.53.0-rc.0" + default: true + } +} diff --git a/influxdb/go.mod b/influxdb/go.mod new file mode 100644 index 000000000..dd29e29c8 --- /dev/null +++ b/influxdb/go.mod @@ -0,0 +1,36 @@ +module github.com/perses/plugins/influxdb + +go 1.25.5 + +require ( + github.com/perses/perses v0.53.0-rc.0 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/zitadel/oidc/v3 v3.45.1 // indirect + github.com/zitadel/schema v1.3.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/influxdb/go.sum b/influxdb/go.sum new file mode 100644 index 000000000..5eaf5d735 --- /dev/null +++ b/influxdb/go.sum @@ -0,0 +1,68 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nexucis/lamenv v0.5.2 h1:tK/u3XGhCq9qIoVNcXsK9LZb8fKopm0A5weqSRvHd7M= +github.com/nexucis/lamenv v0.5.2/go.mod h1:HusJm6ltmmT7FMG8A750mOLuME6SHCsr2iFYxp5fFi0= +github.com/perses/perses v0.53.0-rc.0 h1:f3V1j6EqnKyXUY0mNt4Zp/T6+5U/5SjtCzLHxj9sJDQ= +github.com/perses/perses v0.53.0-rc.0/go.mod h1:q+gB4M2yT//cO6GlCjhOTJLDoSrqtkMLul72Z0WOueI= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/zitadel/oidc/v3 v3.45.1 h1:x7J8NywTUtLR9T5uu2dufae3gJrl6VVpIfvGZy+kzJg= +github.com/zitadel/oidc/v3 v3.45.1/go.mod h1:oFArtAPTXEA4ajkIe/JfBjv7hhlD0kr///UqaO3Uzd0= +github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= +github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/influxdb/jest.config.ts b/influxdb/jest.config.ts new file mode 100644 index 000000000..1c7f61ce7 --- /dev/null +++ b/influxdb/jest.config.ts @@ -0,0 +1,7 @@ +import type { Config } from 'jest'; +import sharedConfig from '../jest.shared'; +const config: Config = { + ...sharedConfig, + displayName: 'influxdb', +}; +export default config; diff --git a/influxdb/package.json b/influxdb/package.json new file mode 100644 index 000000000..c25800052 --- /dev/null +++ b/influxdb/package.json @@ -0,0 +1,69 @@ +{ + "name": "@perses-dev/influxdb-plugin", + "version": "0.1.0-rc.0", + "homepage": "https://github.com/perses/plugins/blob/main/README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/perses/plugins.git" + }, + "bugs": { + "url": "https://github.com/perses/plugins/issues" + }, + "scripts": { + "dev": "rsbuild dev", + "build": "npm run build-mf && concurrently \"npm:build:*\"", + "build-mf": "rsbuild build", + "build:cjs": "swc ./src -d dist/lib/cjs --strip-leading-paths --config-file ../.cjs.swcrc", + "build:esm": "swc ./src -d dist/lib --strip-leading-paths --config-file ../.swcrc", + "build:types": "tsc --project tsconfig.build.json", + "lint": "eslint src --ext .ts,.tsx", + "test": "cross-env LC_ALL=C TZ=UTC jest", + "type-check": "tsc --noEmit" + }, + "main": "lib/cjs/index.js", + "module": "lib/index.js", + "types": "lib/index.d.ts", + "dependencies": {}, + "peerDependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@mui/material": "^5.0.0", + "@hookform/resolvers": "^3.2.0", + "@perses-dev/components": "^0.53.0-rc.1", + "@perses-dev/core": "^0.53.0-beta.4", + "@perses-dev/dashboards": "^0.53.0-rc.1", + "@perses-dev/explore": "^0.53.0-rc.1", + "@perses-dev/plugin-system": "^0.53.0-rc.1", + "@tanstack/react-query": "^4.39.1", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "echarts": "5.5.0", + "immer": "^10.1.1", + "lodash": "^4.17.21", + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0", + "react-hook-form": "^7.52.2", + "react-router-dom": "^6.27.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@swc/cli": "^0.4.1-nightly.20240914", + "@swc/core": "^1.7.28", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^12.1.4", + "@testing-library/user-event": "^14.5.2", + "@types/color-hash": "^2.0.0", + "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.13", + "@types/react": "^18.3.12", + "concurrently": "^8.2.2", + "cross-env": "^7.0.3", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "typescript": "^5.6.3" + }, + "files": [ + "dist", + "README.md" + ] +} diff --git a/influxdb/rsbuild.config.ts b/influxdb/rsbuild.config.ts new file mode 100644 index 000000000..c4127b520 --- /dev/null +++ b/influxdb/rsbuild.config.ts @@ -0,0 +1,25 @@ +import { pluginReact } from '@rsbuild/plugin-react'; +import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack'; +export default { + plugins: [pluginReact()], + tools: { + rspack: { + plugins: [ + new ModuleFederationPlugin({ + name: 'influxdb', + filename: 'remoteEntry.js', + exposes: { + './plugin': './src/getPluginModule.ts', + }, + shared: { + react: { singleton: true, requiredVersion: false }, + 'react-dom': { singleton: true, requiredVersion: false }, + '@perses-dev/core': { singleton: true, requiredVersion: false }, + '@perses-dev/plugin-system': { singleton: true, requiredVersion: false }, + '@perses-dev/components': { singleton: true, requiredVersion: false }, + }, + }), + ], + }, + }, +}; diff --git a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-direct.yaml b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-direct.yaml new file mode 100644 index 000000000..31dba62ff --- /dev/null +++ b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-direct.yaml @@ -0,0 +1,11 @@ +kind: GlobalDatasource +metadata: + name: influxdb-v1-demo +spec: + default: false + plugin: + kind: InfluxDBV1Datasource + spec: + directUrl: http://localhost:8086 + version: v1 + database: mydb diff --git a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-proxy.yaml b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-proxy.yaml new file mode 100644 index 000000000..af399521d --- /dev/null +++ b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-proxy.yaml @@ -0,0 +1,22 @@ +kind: GlobalDatasource +metadata: + name: influxdb-v1-proxy +spec: + default: false + plugin: + kind: InfluxDBV1Datasource + spec: + version: v1 + database: mydb + proxy: + kind: HTTPProxy + spec: + url: http://localhost:8086 + allowedEndpoints: + - endpointPattern: /query + method: GET + - endpointPattern: /query + method: POST + - endpointPattern: /write + method: POST + secret: influxdb-secret diff --git a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-direct.yaml b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-direct.yaml new file mode 100644 index 000000000..f47cfc936 --- /dev/null +++ b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-direct.yaml @@ -0,0 +1,12 @@ +kind: GlobalDatasource +metadata: + name: influxdb-v3-demo +spec: + default: false + plugin: + kind: InfluxDBV3Datasource + spec: + directUrl: http://localhost:8086 + version: v3 + organization: myorg + bucket: mybucket diff --git a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-proxy.yaml b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-proxy.yaml new file mode 100644 index 000000000..185611cf6 --- /dev/null +++ b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-proxy.yaml @@ -0,0 +1,21 @@ +kind: GlobalDatasource +metadata: + name: influxdb-v3-proxy +spec: + default: false + plugin: + kind: InfluxDBV3Datasource + spec: + version: v3 + organization: myorg + bucket: mybucket + proxy: + kind: HTTPProxy + spec: + url: http://localhost:8086 + allowedEndpoints: + - endpointPattern: /api/v3/query_sql + method: POST + - endpointPattern: /api/v3/query_influxql + method: POST + secret: influxdb-secret diff --git a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.cue b/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.cue new file mode 100644 index 000000000..220e7fef4 --- /dev/null +++ b/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.cue @@ -0,0 +1,34 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "github.com/perses/shared/cue/common" + commonProxy "github.com/perses/shared/cue/common/proxy" +) + +kind: "InfluxDBV1Datasource" +spec: { + #directUrl | #proxy + version: "v1" + database: string +} + +#directUrl: { + directUrl: common.#url +} + +#proxy: { + proxy: commonProxy.#HTTPProxy +} \ No newline at end of file diff --git a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.json b/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.json new file mode 100644 index 000000000..3100cacbe --- /dev/null +++ b/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.json @@ -0,0 +1,8 @@ +{ + "kind": "InfluxDBV1Datasource", + "spec": { + "directUrl": "http://localhost:8086", + "version": "v1", + "database": "mydb" + } +} \ No newline at end of file diff --git a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.cue b/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.cue new file mode 100644 index 000000000..575fb027e --- /dev/null +++ b/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.cue @@ -0,0 +1,35 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "github.com/perses/shared/cue/common" + commonProxy "github.com/perses/shared/cue/common/proxy" +) + +kind: "InfluxDBV3Datasource" +spec: { + #directUrl | #proxy + version: "v3" + organization: string + bucket: string +} + +#directUrl: { + directUrl: common.#url +} + +#proxy: { + proxy: commonProxy.#HTTPProxy +} diff --git a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.json b/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.json new file mode 100644 index 000000000..b24d46caf --- /dev/null +++ b/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.json @@ -0,0 +1,9 @@ +{ + "kind": "InfluxDBV3Datasource", + "spec": { + "directUrl": "http://localhost:8086", + "version": "v3", + "organization": "myorg", + "bucket": "mybucket" + } +} diff --git a/influxdb/sdk/go/datasource/datasource.go b/influxdb/sdk/go/datasource/datasource.go new file mode 100644 index 000000000..120540e12 --- /dev/null +++ b/influxdb/sdk/go/datasource/datasource.go @@ -0,0 +1,158 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datasource + +import ( + "encoding/json" + "fmt" + + "github.com/perses/perses/go-sdk/datasource" + "github.com/perses/perses/pkg/model/api/v1/datasource/http" +) + +const ( + PluginKindV1 = "InfluxDBV1Datasource" + PluginKindV3 = "InfluxDBV3Datasource" +) + +// InfluxDBVersion represents the InfluxDB version +type InfluxDBVersion string + +const ( + VersionV1 InfluxDBVersion = "v1" + VersionV3 InfluxDBVersion = "v3" +) + +type PluginSpec struct { + DirectURL string `json:"directUrl,omitempty" yaml:"directUrl,omitempty"` + Proxy *http.Proxy `json:"proxy,omitempty" yaml:"proxy,omitempty"` + Version InfluxDBVersion `json:"version" yaml:"version"` + // V1.8 specific fields + Database string `json:"database,omitempty" yaml:"database,omitempty"` + // V3 specific fields + Organization string `json:"organization,omitempty" yaml:"organization,omitempty"` + Bucket string `json:"bucket,omitempty" yaml:"bucket,omitempty"` +} + +func (s *PluginSpec) UnmarshalJSON(data []byte) error { + type plain PluginSpec + var tmp PluginSpec + if err := json.Unmarshal(data, (*plain)(&tmp)); err != nil { + return err + } + if err := (&tmp).validate(); err != nil { + return err + } + *s = tmp + return nil +} + +func (s *PluginSpec) UnmarshalYAML(unmarshal func(interface{}) error) error { + var tmp PluginSpec + type plain PluginSpec + if err := unmarshal((*plain)(&tmp)); err != nil { + return err + } + if err := (&tmp).validate(); err != nil { + return err + } + *s = tmp + return nil +} + +func (s *PluginSpec) validate() error { + if len(s.DirectURL) == 0 && s.Proxy == nil { + return fmt.Errorf("directUrl or proxy cannot be empty") + } + if len(s.DirectURL) > 0 && s.Proxy != nil { + return fmt.Errorf("at most directUrl or proxy must be configured") + } + if s.Version != VersionV1 && s.Version != VersionV3 { + return fmt.Errorf("version must be either 'v1' or 'v3'") + } + if s.Version == VersionV1 && len(s.Database) == 0 { + return fmt.Errorf("database is required for InfluxDB v1.8") + } + if s.Version == VersionV3 { + if len(s.Organization) == 0 { + return fmt.Errorf("organization is required for InfluxDB v3") + } + if len(s.Bucket) == 0 { + return fmt.Errorf("bucket is required for InfluxDB v3") + } + } + return nil +} + +type Option func(plugin *Builder) error + +func create(options ...Option) (Builder, error) { + builder := &Builder{ + PluginSpec: PluginSpec{}, + } + + var defaults []Option + + for _, opt := range append(defaults, options...) { + if err := opt(builder); err != nil { + return *builder, err + } + } + + return *builder, nil +} + +type Builder struct { + PluginSpec `json:",inline" yaml:",inline"` +} + +func InfluxDBV1(options ...Option) datasource.Option { + return func(builder *datasource.Builder) error { + plugin, err := create(options...) + if err != nil { + return err + } + + builder.Spec.Plugin.Kind = PluginKindV1 + builder.Spec.Plugin.Spec = plugin.PluginSpec + return nil + } +} + +func InfluxDBV3(options ...Option) datasource.Option { + return func(builder *datasource.Builder) error { + plugin, err := create(options...) + if err != nil { + return err + } + + builder.Spec.Plugin.Kind = PluginKindV3 + builder.Spec.Plugin.Spec = plugin.PluginSpec + return nil + } +} + +func SelectorV1(datasourceName string) *datasource.Selector { + return &datasource.Selector{ + Kind: PluginKindV1, + Name: datasourceName, + } +} + +func SelectorV3(datasourceName string) *datasource.Selector { + return &datasource.Selector{ + Kind: PluginKindV3, + Name: datasourceName, + } +} diff --git a/influxdb/sdk/go/datasource/datasource_test.go b/influxdb/sdk/go/datasource/datasource_test.go new file mode 100644 index 000000000..438ef384d --- /dev/null +++ b/influxdb/sdk/go/datasource/datasource_test.go @@ -0,0 +1,201 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datasource + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPluginSpecValidation(t *testing.T) { + tests := []struct { + name string + spec PluginSpec + expectError bool + errorMsg string + }{ + { + name: "valid V1.8 with directUrl", + spec: PluginSpec{ + DirectURL: "http://localhost:8086", + Version: VersionV1, + Database: "mydb", + }, + expectError: false, + }, + { + name: "valid V3 with directUrl", + spec: PluginSpec{ + DirectURL: "http://localhost:8086", + Version: VersionV3, + Organization: "myorg", + Bucket: "mybucket", + }, + expectError: false, + }, + { + name: "missing directUrl and proxy", + spec: PluginSpec{ + Version: VersionV1, + Database: "mydb", + }, + expectError: true, + errorMsg: "directUrl or proxy cannot be empty", + }, + { + name: "missing version", + spec: PluginSpec{ + DirectURL: "http://localhost:8086", + Database: "mydb", + }, + expectError: true, + errorMsg: "version must be either 'v1' or 'v3'", + }, + { + name: "invalid version", + spec: PluginSpec{ + DirectURL: "http://localhost:8086", + Version: "v2", + Database: "mydb", + }, + expectError: true, + errorMsg: "version must be either 'v1' or 'v3'", + }, + { + name: "V1 missing database", + spec: PluginSpec{ + DirectURL: "http://localhost:8086", + Version: VersionV1, + }, + expectError: true, + errorMsg: "database is required for InfluxDB v1.8", + }, + { + name: "V3 missing organization", + spec: PluginSpec{ + DirectURL: "http://localhost:8086", + Version: VersionV3, + Bucket: "mybucket", + }, + expectError: true, + errorMsg: "organization is required for InfluxDB v3", + }, + { + name: "V3 missing bucket", + spec: PluginSpec{ + DirectURL: "http://localhost:8086", + Version: VersionV3, + Organization: "myorg", + }, + expectError: true, + errorMsg: "bucket is required for InfluxDB v3", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.spec.validate() + if tt.expectError { + assert.Error(t, err) + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestPluginSpecUnmarshalJSON(t *testing.T) { + tests := []struct { + name string + json string + expectError bool + expected *PluginSpec + }{ + { + name: "valid V1.8 JSON", + json: `{ + "directUrl": "http://localhost:8086", + "version": "v1", + "database": "mydb" + }`, + expectError: false, + expected: &PluginSpec{ + DirectURL: "http://localhost:8086", + Version: VersionV1, + Database: "mydb", + }, + }, + { + name: "valid V3 JSON", + json: `{ + "directUrl": "http://localhost:8086", + "version": "v3", + "organization": "myorg", + "bucket": "mybucket" + }`, + expectError: false, + expected: &PluginSpec{ + DirectURL: "http://localhost:8086", + Version: VersionV3, + Organization: "myorg", + Bucket: "mybucket", + }, + }, + { + name: "invalid JSON - missing required field", + json: `{ + "directUrl": "http://localhost:8086", + "version": "v1" + }`, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var spec PluginSpec + err := json.Unmarshal([]byte(tt.json), &spec) + if tt.expectError { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected.DirectURL, spec.DirectURL) + assert.Equal(t, tt.expected.Version, spec.Version) + assert.Equal(t, tt.expected.Database, spec.Database) + assert.Equal(t, tt.expected.Organization, spec.Organization) + assert.Equal(t, tt.expected.Bucket, spec.Bucket) + } + }) + } +} + +func TestSelectors(t *testing.T) { + t.Run("SelectorV1", func(t *testing.T) { + selector := SelectorV1("test-influxdb-v1") + assert.Equal(t, PluginKindV1, selector.Kind) + assert.Equal(t, "test-influxdb-v1", selector.Name) + }) + + t.Run("SelectorV3", func(t *testing.T) { + selector := SelectorV3("test-influxdb-v3") + assert.Equal(t, PluginKindV3, selector.Kind) + assert.Equal(t, "test-influxdb-v3", selector.Name) + }) +} diff --git a/influxdb/sdk/go/datasource/options.go b/influxdb/sdk/go/datasource/options.go new file mode 100644 index 000000000..e583cb59b --- /dev/null +++ b/influxdb/sdk/go/datasource/options.go @@ -0,0 +1,66 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datasource + +import ( + "github.com/perses/perses/pkg/model/api/v1/datasource/http" +) + +// DirectURL sets the direct URL for the InfluxDB datasource +func DirectURL(url string) Option { + return func(builder *Builder) error { + builder.DirectURL = url + return nil + } +} + +// HTTPProxy sets the proxy configuration for the InfluxDB datasource +func HTTPProxy(proxy *http.Proxy) Option { + return func(builder *Builder) error { + builder.Proxy = proxy + return nil + } +} + +// Version sets the InfluxDB version +func Version(version InfluxDBVersion) Option { + return func(builder *Builder) error { + builder.Version = version + return nil + } +} + +// Database sets the database name for InfluxDB V1.8 +func Database(database string) Option { + return func(builder *Builder) error { + builder.Database = database + return nil + } +} + +// Organization sets the organization for InfluxDB V3 +func Organization(organization string) Option { + return func(builder *Builder) error { + builder.Organization = organization + return nil + } +} + +// Bucket sets the bucket for InfluxDB V3 +func Bucket(bucket string) Option { + return func(builder *Builder) error { + builder.Bucket = bucket + return nil + } +} diff --git a/influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts b/influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts new file mode 100644 index 000000000..e354e6ed3 --- /dev/null +++ b/influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts @@ -0,0 +1,43 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { fetch, RequestHeaders } from '@perses-dev/core'; +import { DatasourcePlugin } from '@perses-dev/plugin-system'; +import { InfluxDBV1Spec, InfluxDBClient, InfluxDBV1Response, InfluxDBV3Response } from '../../model'; +import { InfluxDBV1Editor } from './InfluxDBV1Editor'; +const createClient: DatasourcePlugin['createClient'] = (spec, options) => { + const { directUrl } = spec; + const { proxyUrl } = options; + const datasourceUrl = directUrl ?? proxyUrl; + if (!datasourceUrl) { + throw new Error('No URL specified for InfluxDB v1.8 client'); + } + return { + options: { datasourceUrl }, + queryV1: async (query: string, database: string, headers?: RequestHeaders): Promise => { + const url = new URL('/query', datasourceUrl); + url.searchParams.set('db', database); + url.searchParams.set('q', query); + const response = await fetch(url.toString(), { method: 'GET', headers }); + if (!response.ok) throw new Error('InfluxDB v1.8 query failed: ' + response.statusText); + return response.json(); + }, + queryV3SQL: async (): Promise => { + throw new Error('InfluxDB v3 queries not supported on v1.8 datasource'); + }, + }; +}; +export const InfluxDBV1Datasource: DatasourcePlugin = { + createClient, + createInitialOptions: () => ({ version: 'v1', database: '' }), + OptionsEditorComponent: InfluxDBV1Editor, +}; diff --git a/influxdb/src/datasources/influxdb-v1/InfluxDBV1Editor.tsx b/influxdb/src/datasources/influxdb-v1/InfluxDBV1Editor.tsx new file mode 100644 index 000000000..74fc122c6 --- /dev/null +++ b/influxdb/src/datasources/influxdb-v1/InfluxDBV1Editor.tsx @@ -0,0 +1,38 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { TextField, Box } from '@mui/material'; +import { OptionsEditorProps } from '@perses-dev/plugin-system'; +import { InfluxDBV1Spec } from '../../model'; +export function InfluxDBV1Editor({ value, onChange }: OptionsEditorProps) { + return ( + + onChange({ ...value, directUrl: e.target.value })} + helperText="Optional: URL to access InfluxDB directly from the browser" + fullWidth + /> + onChange({ ...value, database: e.target.value })} + helperText="The InfluxDB database to query" + required + fullWidth + /> + + ); +} diff --git a/influxdb/src/datasources/influxdb-v1/index.ts b/influxdb/src/datasources/influxdb-v1/index.ts new file mode 100644 index 000000000..fc5eff284 --- /dev/null +++ b/influxdb/src/datasources/influxdb-v1/index.ts @@ -0,0 +1,15 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export * from './InfluxDBV1Datasource'; +export * from './InfluxDBV1Editor'; diff --git a/influxdb/src/datasources/influxdb-v3/InfluxDBV3Datasource.ts b/influxdb/src/datasources/influxdb-v3/InfluxDBV3Datasource.ts new file mode 100644 index 000000000..bac3fd662 --- /dev/null +++ b/influxdb/src/datasources/influxdb-v3/InfluxDBV3Datasource.ts @@ -0,0 +1,47 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { fetch, RequestHeaders } from '@perses-dev/core'; +import { DatasourcePlugin } from '@perses-dev/plugin-system'; +import { InfluxDBV3Spec, InfluxDBClient, InfluxDBV1Response, InfluxDBV3Response } from '../../model'; +import { InfluxDBV3Editor } from './InfluxDBV3Editor'; + +const createClient: DatasourcePlugin['createClient'] = (spec, options) => { + const { directUrl } = spec; + const { proxyUrl } = options; + const datasourceUrl = directUrl ?? proxyUrl; + if (!datasourceUrl) { + throw new Error('No URL specified for InfluxDB v3 client'); + } + return { + options: { datasourceUrl }, + queryV1: async (): Promise => { + throw new Error('InfluxDB v1.8 queries not supported on v3 datasource'); + }, + queryV3SQL: async (query: string, organization: string, bucket: string, headers?: RequestHeaders): Promise => { + const url = new URL('/api/v3/query_sql', datasourceUrl); + const response = await fetch(url.toString(), { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...headers }, + body: JSON.stringify({ query, query_type: 'sql', params: { organization, bucket } }), + }); + if (!response.ok) throw new Error('InfluxDB v3 query failed: ' + response.statusText); + return response.json(); + }, + }; +}; +export const InfluxDBV3Datasource: DatasourcePlugin = { + createClient, + createInitialOptions: () => ({ version: 'v3', organization: '', bucket: '' }), + OptionsEditorComponent: InfluxDBV3Editor, +}; diff --git a/influxdb/src/datasources/influxdb-v3/InfluxDBV3Editor.tsx b/influxdb/src/datasources/influxdb-v3/InfluxDBV3Editor.tsx new file mode 100644 index 000000000..16f619104 --- /dev/null +++ b/influxdb/src/datasources/influxdb-v3/InfluxDBV3Editor.tsx @@ -0,0 +1,47 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { TextField, Box } from '@mui/material'; +import { OptionsEditorProps } from '@perses-dev/plugin-system'; +import { InfluxDBV3Spec } from '../../model'; +export function InfluxDBV3Editor({ value, onChange }: OptionsEditorProps) { + return ( + + onChange({ ...value, directUrl: e.target.value })} + helperText="Optional: URL to access InfluxDB directly from the browser" + fullWidth + /> + onChange({ ...value, organization: e.target.value })} + helperText="The InfluxDB organization" + required + fullWidth + /> + onChange({ ...value, bucket: e.target.value })} + helperText="The InfluxDB bucket to query" + required + fullWidth + /> + + ); +} diff --git a/influxdb/src/datasources/influxdb-v3/index.ts b/influxdb/src/datasources/influxdb-v3/index.ts new file mode 100644 index 000000000..562f7e30d --- /dev/null +++ b/influxdb/src/datasources/influxdb-v3/index.ts @@ -0,0 +1,15 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export * from './InfluxDBV3Datasource'; +export * from './InfluxDBV3Editor'; diff --git a/influxdb/src/getPluginModule.ts b/influxdb/src/getPluginModule.ts new file mode 100644 index 000000000..4b2ca4b2d --- /dev/null +++ b/influxdb/src/getPluginModule.ts @@ -0,0 +1,11 @@ +import { PluginModuleResource, DatasourcePluginModule, TimeSeriesQueryPluginModule } from '@perses-dev/plugin-system'; +import { InfluxDBV1Datasource } from './datasources/influxdb-v1'; +import { InfluxDBV3Datasource } from './datasources/influxdb-v3'; +import { InfluxDBTimeSeriesQuery } from './queries/influxdb-time-series-query'; +export function getPluginModules(): PluginModuleResource[] { + return [ + { kind: 'Datasource', plugin: InfluxDBV1Datasource, pluginKind: 'InfluxDBV1Datasource' } as DatasourcePluginModule, + { kind: 'Datasource', plugin: InfluxDBV3Datasource, pluginKind: 'InfluxDBV3Datasource' } as DatasourcePluginModule, + { kind: 'TimeSeriesQuery', plugin: InfluxDBTimeSeriesQuery, pluginKind: 'InfluxDBTimeSeriesQuery' } as TimeSeriesQueryPluginModule, + ]; +} diff --git a/influxdb/src/index.ts b/influxdb/src/index.ts new file mode 100644 index 000000000..4f0fd2bb5 --- /dev/null +++ b/influxdb/src/index.ts @@ -0,0 +1,4 @@ +export * from './datasources/influxdb-v1'; +export * from './datasources/influxdb-v3'; +export * from './queries/influxdb-time-series-query'; +export * from './model'; diff --git a/influxdb/src/model/index.ts b/influxdb/src/model/index.ts new file mode 100644 index 000000000..8f24c261d --- /dev/null +++ b/influxdb/src/model/index.ts @@ -0,0 +1,13 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +export * from './influxdb-types'; diff --git a/influxdb/src/model/influxdb-types.ts b/influxdb/src/model/influxdb-types.ts new file mode 100644 index 000000000..d4d4a19d5 --- /dev/null +++ b/influxdb/src/model/influxdb-types.ts @@ -0,0 +1,59 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { HTTPProxy, RequestHeaders } from '@perses-dev/core'; +import { DatasourceClient } from '@perses-dev/plugin-system'; +export type InfluxDBVersion = 'v1' | 'v3'; +export interface InfluxDBV1Spec { + directUrl?: string; + proxy?: HTTPProxy; + version: 'v1'; + database: string; +} +export interface InfluxDBV3Spec { + directUrl?: string; + proxy?: HTTPProxy; + version: 'v3'; + organization: string; + bucket: string; +} +export type InfluxDBDatasourceSpec = InfluxDBV1Spec | InfluxDBV3Spec; +interface InfluxDBClientOptions { + datasourceUrl: string; + headers?: RequestHeaders; +} +export interface InfluxDBV1Response { + results: Array<{ + statement_id?: number; + series?: Array<{ + name: string; + columns: string[]; + values: any[][]; + tags?: Record; + }>; + error?: string; + }>; +} +export interface InfluxDBV3Response { + schema: { + fields: Array<{ + name: string; + data_type: string; + }>; + }; + data: any[][]; +} +export interface InfluxDBClient extends DatasourceClient { + options: InfluxDBClientOptions; + queryV1(query: string, database: string, headers?: RequestHeaders): Promise; + queryV3SQL(query: string, organization: string, bucket: string, headers?: RequestHeaders): Promise; +} diff --git a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts new file mode 100644 index 000000000..a67216cc2 --- /dev/null +++ b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts @@ -0,0 +1,85 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { TimeSeriesQueryPlugin } from '@perses-dev/plugin-system'; +import { TimeSeriesData } from '@perses-dev/core'; +import { InfluxDBDatasourceSpec, InfluxDBClient } from '../../model'; +import { InfluxDBTimeSeriesQueryEditor } from './InfluxDBTimeSeriesQueryEditor'; +export interface InfluxDBTimeSeriesQuerySpec { + query: string; +} +function convertV1ResponseToTimeSeries(response: any): TimeSeriesData { + const datasets: any[] = []; + if (response.results && response.results[0] && response.results[0].series) { + response.results[0].series.forEach((series: any) => { + const timeIndex = series.columns.indexOf('time'); + const valueColumns = series.columns.filter((col: string) => col !== 'time'); + valueColumns.forEach((valueColumn: string) => { + const valueIndex = series.columns.indexOf(valueColumn); + const data = series.values.map((row: any[]) => ({ + x: new Date(row[timeIndex]).getTime(), + y: row[valueIndex], + })); + const tagStr = series.tags + ? Object.entries(series.tags).map(([k, v]) => k + '="' + v + '"').join(',') + : ''; + datasets.push({ + name: series.name + '.' + valueColumn, + values: data, + formattedName: tagStr + ? series.name + '{' + tagStr + '}.' + valueColumn + : series.name + '.' + valueColumn, + }); + }); + }); + } + return { datasets }; +} +function convertV3ResponseToTimeSeries(response: any): TimeSeriesData { + const datasets: any[] = []; + if (response.data && response.schema) { + const timeField = response.schema.fields.find((f: any) => f.data_type === 'Timestamp'); + const timeIndex = response.schema.fields.indexOf(timeField); + response.schema.fields.forEach((field: any, index: number) => { + if (field.data_type !== 'Timestamp' && field.data_type !== 'Utf8') { + const data = response.data.map((row: any[]) => ({ + x: new Date(row[timeIndex]).getTime(), + y: row[index], + })); + datasets.push({ name: field.name, values: data, formattedName: field.name }); + } + }); + } + return { datasets }; +} +export const InfluxDBTimeSeriesQuery: TimeSeriesQueryPlugin< + InfluxDBTimeSeriesQuerySpec, + InfluxDBDatasourceSpec, + InfluxDBClient +> = { + getTimeSeriesData: async (spec, context) => { + const { query } = spec; + const { datasourceClient, datasourceSpec } = context; + if (!datasourceClient) { + throw new Error('No datasource client available'); + } + if (datasourceSpec.version === 'v1') { + const response = await datasourceClient.queryV1(query, datasourceSpec.database); + return convertV1ResponseToTimeSeries(response); + } else if (datasourceSpec.version === 'v3') { + const response = await datasourceClient.queryV3SQL(query, datasourceSpec.organization, datasourceSpec.bucket); + return convertV3ResponseToTimeSeries(response); + } + throw new Error('Unsupported InfluxDB version: ' + (datasourceSpec as any).version); + }, + OptionsEditorComponent: InfluxDBTimeSeriesQueryEditor, +}; diff --git a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQueryEditor.tsx b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQueryEditor.tsx new file mode 100644 index 000000000..d23de26f1 --- /dev/null +++ b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQueryEditor.tsx @@ -0,0 +1,32 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { TextField, Box } from '@mui/material'; +import { OptionsEditorProps } from '@perses-dev/plugin-system'; +import { InfluxDBTimeSeriesQuerySpec } from './InfluxDBTimeSeriesQuery'; +export function InfluxDBTimeSeriesQueryEditor({ value, onChange }: OptionsEditorProps) { + return ( + + onChange({ ...value, query: e.target.value })} + helperText="SQL query for InfluxDB v3 or InfluxQL for v1.8" + required + fullWidth + multiline + rows={6} + /> + + ); +} diff --git a/influxdb/src/queries/influxdb-time-series-query/index.ts b/influxdb/src/queries/influxdb-time-series-query/index.ts new file mode 100644 index 000000000..6c18f5c2c --- /dev/null +++ b/influxdb/src/queries/influxdb-time-series-query/index.ts @@ -0,0 +1,15 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export * from './InfluxDBTimeSeriesQuery'; +export * from './InfluxDBTimeSeriesQueryEditor'; diff --git a/influxdb/tsconfig.build.json b/influxdb/tsconfig.build.json new file mode 100644 index 000000000..09ca62b17 --- /dev/null +++ b/influxdb/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist/lib" + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.stories.tsx"] +} diff --git a/influxdb/tsconfig.json b/influxdb/tsconfig.json new file mode 100644 index 000000000..3c43903cf --- /dev/null +++ b/influxdb/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} From 324517c04089f48c5023637c64e65e7b0e511054 Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Sun, 15 Feb 2026 19:16:33 +0100 Subject: [PATCH 2/8] feat: Update the plugin Signed-off-by: Pascal Zimmermann --- influxdb/README.md | 28 + influxdb/cue.mod/module.cue | 8 +- influxdb/package-lock.json | 6825 +++++++++++++++++ influxdb/package.json | 25 +- influxdb/rsbuild.config.ts | 41 +- ...ux-db-v3-datasource.cue => datasource.cue} | 18 +- .../influx-db-v3-datasource.json | 9 - .../bootstrap.ts} | 23 +- influxdb/src/getPluginModule.ts | 39 +- influxdb/src/index-federation.ts | 2 + .../InfluxDBTimeSeriesQuery.ts | 81 +- 11 files changed, 6992 insertions(+), 107 deletions(-) create mode 100644 influxdb/README.md create mode 100644 influxdb/package-lock.json rename influxdb/schemas/datasources/influx-db-datasource/{influx-db-v3-datasource.cue => datasource.cue} (77%) delete mode 100644 influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.json rename influxdb/{schemas/datasources/influx-db-datasource/influx-db-v1-datasource.cue => src/bootstrap.ts} (65%) create mode 100644 influxdb/src/index-federation.ts diff --git a/influxdb/README.md b/influxdb/README.md new file mode 100644 index 000000000..90bbfa098 --- /dev/null +++ b/influxdb/README.md @@ -0,0 +1,28 @@ +# InfluxDB Plugin + +Plugin for InfluxDB datasource support in Perses. + +## Features + +- InfluxDB v1.x datasource support +- InfluxDB v3 datasource support +- Time series query support + +## Installation + +```bash +npm install @perses-dev/influxdb-plugin +``` + +## Development + +```bash +npm run dev +``` + +## Build + +```bash +npm run build +``` + diff --git a/influxdb/cue.mod/module.cue b/influxdb/cue.mod/module.cue index b5be31c96..aa84626aa 100644 --- a/influxdb/cue.mod/module.cue +++ b/influxdb/cue.mod/module.cue @@ -1,13 +1,13 @@ module: "github.com/perses/plugins/influxdb@v0" language: { - version: "v0.8.0" -} -source: { - kind: "git" + version: "v0.9.0-alpha.0" } deps: { "github.com/perses/perses/cue@v0": { v: "v0.53.0-rc.0" default: true } + "github.com/perses/shared/cue@v0": { + v: "v0.53.0-rc.1" + } } diff --git a/influxdb/package-lock.json b/influxdb/package-lock.json new file mode 100644 index 000000000..e5766aeaa --- /dev/null +++ b/influxdb/package-lock.json @@ -0,0 +1,6825 @@ +{ + "name": "@perses-dev/influxdb-plugin", + "version": "0.1.0-rc.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@perses-dev/influxdb-plugin", + "version": "0.1.0-rc.0", + "devDependencies": { + "@swc/cli": "^0.4.1-nightly.20240914", + "@swc/core": "^1.7.28", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^12.1.4", + "@testing-library/user-event": "^14.5.2", + "@types/color-hash": "^2.0.0", + "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.13", + "@types/react": "^18.3.12", + "concurrently": "^8.2.2", + "cross-env": "^7.0.3", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "typescript": "^5.6.3" + }, + "peerDependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@hookform/resolvers": "^3.2.0", + "@mui/material": "^5.0.0", + "@perses-dev/components": "^0.53.0-rc.1", + "@perses-dev/core": "^0.53.0-beta.4", + "@perses-dev/dashboards": "^0.53.0-rc.1", + "@perses-dev/explore": "^0.53.0-rc.1", + "@perses-dev/plugin-system": "^0.53.0-rc.1", + "@tanstack/react-query": "^4.39.1", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "echarts": "5.5.0", + "immer": "^10.1.1", + "lodash": "^4.17.21", + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0", + "react-hook-form": "^7.52.2", + "react-router-dom": "^6.27.0", + "zod": "^3.22.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mole-inc/bin-wrapper": { + "version": "8.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bin-check": "^4.1.0", + "bin-version-check": "^5.0.0", + "content-disposition": "^0.5.4", + "ext-name": "^5.0.0", + "file-type": "^17.1.6", + "filenamify": "^5.0.2", + "got": "^11.8.5", + "os-filter-obj": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@swc/cli": { + "version": "0.4.1-nightly.20240914", + "dev": true, + "license": "MIT", + "dependencies": { + "@mole-inc/bin-wrapper": "^8.0.1", + "@swc/counter": "^0.1.3", + "commander": "^8.3.0", + "fast-glob": "^3.2.5", + "minimatch": "^9.0.3", + "piscina": "^4.3.0", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3" + }, + "bin": { + "spack": "bin/spack.js", + "swc": "bin/swc.js", + "swcx": "bin/swcx.js" + }, + "engines": { + "node": ">= 16.14.0" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^3.5.1" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@swc/cli/node_modules/commander": { + "version": "8.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@swc/cli/node_modules/source-map": { + "version": "0.7.6", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@swc/core": { + "version": "1.15.11", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.11", + "@swc/core-darwin-x64": "1.15.11", + "@swc/core-linux-arm-gnueabihf": "1.15.11", + "@swc/core-linux-arm64-gnu": "1.15.11", + "@swc/core-linux-arm64-musl": "1.15.11", + "@swc/core-linux-x64-gnu": "1.15.11", + "@swc/core-linux-x64-musl": "1.15.11", + "@swc/core-win32-arm64-msvc": "1.15.11", + "@swc/core-win32-ia32-msvc": "1.15.11", + "@swc/core-win32-x64-msvc": "1.15.11" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.11", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "12.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.0.0", + "@types/react-dom": "<18.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "<18.0.0", + "react-dom": "<18.0.0" + } + }, + "node_modules/@testing-library/react/node_modules/@testing-library/dom": { + "version": "8.20.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@testing-library/react/node_modules/@types/react-dom": { + "version": "17.0.26", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0" + } + }, + "node_modules/@testing-library/react/node_modules/aria-query": { + "version": "5.1.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/react/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/react/node_modules/deep-equal": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/color-hash": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/abab": { + "version": "2.0.6", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bin-check": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^0.7.0", + "executable": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-version": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version-check": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "bin-version": "^6.0.0", + "semver": "^7.5.3", + "semver-truncate": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version/node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/bin-version/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version/node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/char-regex": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/co": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "8.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/date-fns": { + "version": "2.30.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cssom": { + "version": "0.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "dev": true, + "license": "MIT" + }, + "node_modules/domexception": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.283", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "0.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/execa/node_modules/lru-cache": { + "version": "4.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/execa/node_modules/yallist": { + "version": "2.1.2", + "dev": true, + "license": "ISC" + }, + "node_modules/executable": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ext-list": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-type": { + "version": "17.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0-alpha.9", + "token-types": "^5.0.0-alpha.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^3.0.0", + "strip-outer": "^2.0.0", + "trim-repeated": "^2.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-versions": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-local": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-filter-obj": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/peek-readable": { + "version": "5.4.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/piscina": { + "version": "4.9.2", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/psl": { + "version": "1.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-regex": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "dev": true + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-outer": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.1.3" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-types": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-repeated": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/influxdb/package.json b/influxdb/package.json index c25800052..31382d1f2 100644 --- a/influxdb/package.json +++ b/influxdb/package.json @@ -65,5 +65,28 @@ "files": [ "dist", "README.md" - ] + ], + "perses": { + "schemasPath": "schemas", + "plugins": [ + { + "kind": "Datasource", + "spec": { + "display": { + "name": "InfluxDB Datasource" + }, + "name": "InfluxDBDatasource" + } + }, + { + "kind": "TimeSeriesQuery", + "spec": { + "display": { + "name": "InfluxDB Time Series Query" + }, + "name": "InfluxDBTimeSeriesQuery" + } + } + ] + } } diff --git a/influxdb/rsbuild.config.ts b/influxdb/rsbuild.config.ts index c4127b520..cd888e6fb 100644 --- a/influxdb/rsbuild.config.ts +++ b/influxdb/rsbuild.config.ts @@ -1,25 +1,22 @@ import { pluginReact } from '@rsbuild/plugin-react'; -import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack'; -export default { - plugins: [pluginReact()], - tools: { - rspack: { - plugins: [ - new ModuleFederationPlugin({ - name: 'influxdb', - filename: 'remoteEntry.js', - exposes: { - './plugin': './src/getPluginModule.ts', - }, - shared: { - react: { singleton: true, requiredVersion: false }, - 'react-dom': { singleton: true, requiredVersion: false }, - '@perses-dev/core': { singleton: true, requiredVersion: false }, - '@perses-dev/plugin-system': { singleton: true, requiredVersion: false }, - '@perses-dev/components': { singleton: true, requiredVersion: false }, - }, - }), - ], +import { createConfigForPlugin } from '../rsbuild.shared'; + +export default createConfigForPlugin({ + name: 'InfluxDB', + rsbuildConfig: { + plugins: [pluginReact()], + }, + moduleFederation: { + exposes: { + './InfluxDBDatasource': './src/datasources/influxdb-v1/InfluxDBV1Datasource.ts', + './InfluxDBTimeSeriesQuery': './src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts', + }, + shared: { + react: { singleton: true, requiredVersion: false }, + 'react-dom': { singleton: true, requiredVersion: false }, + '@perses-dev/core': { singleton: true, requiredVersion: false }, + '@perses-dev/plugin-system': { singleton: true, requiredVersion: false }, + '@perses-dev/components': { singleton: true, requiredVersion: false }, }, }, -}; +}); diff --git a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.cue b/influxdb/schemas/datasources/influx-db-datasource/datasource.cue similarity index 77% rename from influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.cue rename to influxdb/schemas/datasources/influx-db-datasource/datasource.cue index 575fb027e..4af074fbe 100644 --- a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.cue +++ b/influxdb/schemas/datasources/influx-db-datasource/datasource.cue @@ -14,12 +14,22 @@ package model import ( - "github.com/perses/shared/cue/common" - commonProxy "github.com/perses/shared/cue/common/proxy" + "github.com/perses/perses/cue/common" + commonProxy "github.com/perses/perses/cue/common/proxy" ) -kind: "InfluxDBV3Datasource" +kind: "InfluxDBDatasource" spec: { + #v1Config | #v3Config +} + +#v1Config: { + #directUrl | #proxy + version: "v1" + database: string +} + +#v3Config: { #directUrl | #proxy version: "v3" organization: string @@ -32,4 +42,4 @@ spec: { #proxy: { proxy: commonProxy.#HTTPProxy -} +} \ No newline at end of file diff --git a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.json b/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.json deleted file mode 100644 index b24d46caf..000000000 --- a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v3-datasource.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "kind": "InfluxDBV3Datasource", - "spec": { - "directUrl": "http://localhost:8086", - "version": "v3", - "organization": "myorg", - "bucket": "mybucket" - } -} diff --git a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.cue b/influxdb/src/bootstrap.ts similarity index 65% rename from influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.cue rename to influxdb/src/bootstrap.ts index 220e7fef4..399b43eb5 100644 --- a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.cue +++ b/influxdb/src/bootstrap.ts @@ -11,24 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package model +export { getPluginModule } from './getPluginModule'; +export { InfluxDBV1Datasource as InfluxDBDatasource } from './datasources/influxdb-v1'; +export { InfluxDBTimeSeriesQuery } from './queries/influxdb-time-series-query'; -import ( - "github.com/perses/shared/cue/common" - commonProxy "github.com/perses/shared/cue/common/proxy" -) - -kind: "InfluxDBV1Datasource" -spec: { - #directUrl | #proxy - version: "v1" - database: string -} - -#directUrl: { - directUrl: common.#url -} - -#proxy: { - proxy: commonProxy.#HTTPProxy -} \ No newline at end of file diff --git a/influxdb/src/getPluginModule.ts b/influxdb/src/getPluginModule.ts index 4b2ca4b2d..cab8b384b 100644 --- a/influxdb/src/getPluginModule.ts +++ b/influxdb/src/getPluginModule.ts @@ -1,11 +1,30 @@ -import { PluginModuleResource, DatasourcePluginModule, TimeSeriesQueryPluginModule } from '@perses-dev/plugin-system'; -import { InfluxDBV1Datasource } from './datasources/influxdb-v1'; -import { InfluxDBV3Datasource } from './datasources/influxdb-v3'; -import { InfluxDBTimeSeriesQuery } from './queries/influxdb-time-series-query'; -export function getPluginModules(): PluginModuleResource[] { - return [ - { kind: 'Datasource', plugin: InfluxDBV1Datasource, pluginKind: 'InfluxDBV1Datasource' } as DatasourcePluginModule, - { kind: 'Datasource', plugin: InfluxDBV3Datasource, pluginKind: 'InfluxDBV3Datasource' } as DatasourcePluginModule, - { kind: 'TimeSeriesQuery', plugin: InfluxDBTimeSeriesQuery, pluginKind: 'InfluxDBTimeSeriesQuery' } as TimeSeriesQueryPluginModule, - ]; +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { PluginModuleResource, PluginModuleSpec } from '@perses-dev/plugin-system'; +import packageJson from '../package.json'; + +/** + * Returns the plugin module information from package.json + */ +export function getPluginModule(): PluginModuleResource { + const { name, version, perses } = packageJson; + return { + kind: 'PluginModule', + metadata: { + name, + version, + }, + spec: perses as PluginModuleSpec, + }; } diff --git a/influxdb/src/index-federation.ts b/influxdb/src/index-federation.ts new file mode 100644 index 000000000..f139775e5 --- /dev/null +++ b/influxdb/src/index-federation.ts @@ -0,0 +1,2 @@ +import('./bootstrap'); + diff --git a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts index a67216cc2..1241bf66d 100644 --- a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts +++ b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts @@ -11,75 +11,82 @@ // See the License for the specific language governing permissions and // limitations under the License. import { TimeSeriesQueryPlugin } from '@perses-dev/plugin-system'; -import { TimeSeriesData } from '@perses-dev/core'; -import { InfluxDBDatasourceSpec, InfluxDBClient } from '../../model'; +import { TimeSeriesData, TimeSeries } from '@perses-dev/core'; import { InfluxDBTimeSeriesQueryEditor } from './InfluxDBTimeSeriesQueryEditor'; export interface InfluxDBTimeSeriesQuerySpec { query: string; } -function convertV1ResponseToTimeSeries(response: any): TimeSeriesData { - const datasets: any[] = []; +function convertV1ResponseToTimeSeries(response: any): TimeSeries[] { + const series: TimeSeries[] = []; if (response.results && response.results[0] && response.results[0].series) { - response.results[0].series.forEach((series: any) => { - const timeIndex = series.columns.indexOf('time'); - const valueColumns = series.columns.filter((col: string) => col !== 'time'); + response.results[0].series.forEach((seriesData: any) => { + const timeIndex = seriesData.columns.indexOf('time'); + const valueColumns = seriesData.columns.filter((col: string) => col !== 'time'); valueColumns.forEach((valueColumn: string) => { - const valueIndex = series.columns.indexOf(valueColumn); - const data = series.values.map((row: any[]) => ({ - x: new Date(row[timeIndex]).getTime(), - y: row[valueIndex], - })); - const tagStr = series.tags - ? Object.entries(series.tags).map(([k, v]) => k + '="' + v + '"').join(',') + const valueIndex = seriesData.columns.indexOf(valueColumn); + const values = seriesData.values.map((row: any[]) => [new Date(row[timeIndex]).getTime(), row[valueIndex]]); + const tagStr = seriesData.tags + ? Object.entries(seriesData.tags) + .map(([k, v]) => k + '="' + v + '"') + .join(',') : ''; - datasets.push({ - name: series.name + '.' + valueColumn, - values: data, + series.push({ + name: seriesData.name + '.' + valueColumn, + values: values as Array<[number, number | null]>, formattedName: tagStr - ? series.name + '{' + tagStr + '}.' + valueColumn - : series.name + '.' + valueColumn, + ? seriesData.name + '{' + tagStr + '}.' + valueColumn + : seriesData.name + '.' + valueColumn, }); }); }); } - return { datasets }; + return series; } -function convertV3ResponseToTimeSeries(response: any): TimeSeriesData { - const datasets: any[] = []; +function convertV3ResponseToTimeSeries(response: any): TimeSeries[] { + const series: TimeSeries[] = []; if (response.data && response.schema) { const timeField = response.schema.fields.find((f: any) => f.data_type === 'Timestamp'); const timeIndex = response.schema.fields.indexOf(timeField); response.schema.fields.forEach((field: any, index: number) => { if (field.data_type !== 'Timestamp' && field.data_type !== 'Utf8') { - const data = response.data.map((row: any[]) => ({ - x: new Date(row[timeIndex]).getTime(), - y: row[index], - })); - datasets.push({ name: field.name, values: data, formattedName: field.name }); + const values = response.data.map((row: any[]) => [new Date(row[timeIndex]).getTime(), row[index]]); + series.push({ + name: field.name, + values: values as Array<[number, number | null]>, + formattedName: field.name, + }); } }); } - return { datasets }; + return series; } -export const InfluxDBTimeSeriesQuery: TimeSeriesQueryPlugin< - InfluxDBTimeSeriesQuerySpec, - InfluxDBDatasourceSpec, - InfluxDBClient -> = { - getTimeSeriesData: async (spec, context) => { +export const InfluxDBTimeSeriesQuery: TimeSeriesQueryPlugin = { + getTimeSeriesData: async (spec: InfluxDBTimeSeriesQuerySpec, context: any) => { const { query } = spec; const { datasourceClient, datasourceSpec } = context; if (!datasourceClient) { throw new Error('No datasource client available'); } + let timeSeries: TimeSeries[] = []; if (datasourceSpec.version === 'v1') { const response = await datasourceClient.queryV1(query, datasourceSpec.database); - return convertV1ResponseToTimeSeries(response); + timeSeries = convertV1ResponseToTimeSeries(response); } else if (datasourceSpec.version === 'v3') { const response = await datasourceClient.queryV3SQL(query, datasourceSpec.organization, datasourceSpec.bucket); - return convertV3ResponseToTimeSeries(response); + timeSeries = convertV3ResponseToTimeSeries(response); + } else { + throw new Error('Unsupported InfluxDB version: ' + (datasourceSpec as any).version); } - throw new Error('Unsupported InfluxDB version: ' + (datasourceSpec as any).version); + + return { + series: timeSeries, + timeRange: context.timeRange, + stepMs: 30 * 1000, + metadata: { + executedQueryString: query, + }, + } as TimeSeriesData; }, OptionsEditorComponent: InfluxDBTimeSeriesQueryEditor, + createInitialOptions: () => ({ query: '' }), }; From f49bc5c0dd9e57f5829c56024b6786e4c8b4d124 Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Mon, 16 Feb 2026 08:56:19 +0100 Subject: [PATCH 3/8] WIP --- influxdb/rsbuild.config.ts | 4 +- influxdb/src/__tests__/ExposeModules.test.ts | 46 ++++++++ .../src/__tests__/InfluxDBDatasource.test.ts | 88 ++++++++++++++ .../InfluxDBPluginIntegration.test.ts | 107 ++++++++++++++++++ .../__tests__/ModuleFederationExports.test.ts | 84 ++++++++++++++ influxdb/src/bootstrap.ts | 7 ++ .../influxdb-v1/InfluxDBV1Datasource.ts | 4 + influxdb/src/expose-datasource.ts | 16 +++ influxdb/src/expose-query.ts | 16 +++ .../InfluxDBTimeSeriesQuery.ts | 4 + 10 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 influxdb/src/__tests__/ExposeModules.test.ts create mode 100644 influxdb/src/__tests__/InfluxDBDatasource.test.ts create mode 100644 influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts create mode 100644 influxdb/src/__tests__/ModuleFederationExports.test.ts create mode 100644 influxdb/src/expose-datasource.ts create mode 100644 influxdb/src/expose-query.ts diff --git a/influxdb/rsbuild.config.ts b/influxdb/rsbuild.config.ts index cd888e6fb..4bd715a63 100644 --- a/influxdb/rsbuild.config.ts +++ b/influxdb/rsbuild.config.ts @@ -8,8 +8,8 @@ export default createConfigForPlugin({ }, moduleFederation: { exposes: { - './InfluxDBDatasource': './src/datasources/influxdb-v1/InfluxDBV1Datasource.ts', - './InfluxDBTimeSeriesQuery': './src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts', + './InfluxDBDatasource': './src/expose-datasource.ts', + './InfluxDBTimeSeriesQuery': './src/expose-query.ts', }, shared: { react: { singleton: true, requiredVersion: false }, diff --git a/influxdb/src/__tests__/ExposeModules.test.ts b/influxdb/src/__tests__/ExposeModules.test.ts new file mode 100644 index 000000000..325083419 --- /dev/null +++ b/influxdb/src/__tests__/ExposeModules.test.ts @@ -0,0 +1,46 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as InfluxDBDatasourceExpose from '../expose-datasource'; +import * as InfluxDBTimeSeriesQueryExpose from '../expose-query'; + +describe('Module Federation Expose Modules', () => { + describe('expose-datasource.ts', () => { + it('should export default InfluxDBDatasource', () => { + expect(InfluxDBDatasourceExpose.default).toBeDefined(); + expect(InfluxDBDatasourceExpose.default).toHaveProperty('createClient'); + }); + + it('should have all required datasource properties', () => { + const datasource = InfluxDBDatasourceExpose.default; + expect(datasource).toHaveProperty('createClient'); + expect(datasource).toHaveProperty('createInitialOptions'); + expect(datasource).toHaveProperty('OptionsEditorComponent'); + }); + }); + + describe('expose-query.ts', () => { + it('should export default InfluxDBTimeSeriesQuery', () => { + expect(InfluxDBTimeSeriesQueryExpose.default).toBeDefined(); + expect(InfluxDBTimeSeriesQueryExpose.default).toHaveProperty('getTimeSeriesData'); + }); + + it('should have all required query properties', () => { + const query = InfluxDBTimeSeriesQueryExpose.default; + expect(query).toHaveProperty('getTimeSeriesData'); + expect(query).toHaveProperty('createInitialOptions'); + expect(query).toHaveProperty('OptionsEditorComponent'); + }); + }); +}); + diff --git a/influxdb/src/__tests__/InfluxDBDatasource.test.ts b/influxdb/src/__tests__/InfluxDBDatasource.test.ts new file mode 100644 index 000000000..48bfd0c45 --- /dev/null +++ b/influxdb/src/__tests__/InfluxDBDatasource.test.ts @@ -0,0 +1,88 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { InfluxDBV1Datasource } from '../datasources/influxdb-v1'; + +describe('InfluxDBDatasource', () => { + it('should have createClient method', () => { + expect(InfluxDBV1Datasource).toHaveProperty('createClient'); + expect(typeof InfluxDBV1Datasource.createClient).toBe('function'); + }); + + it('should have OptionsEditorComponent', () => { + expect(InfluxDBV1Datasource).toHaveProperty('OptionsEditorComponent'); + }); + + it('should have createInitialOptions method', () => { + expect(InfluxDBV1Datasource).toHaveProperty('createInitialOptions'); + expect(typeof InfluxDBV1Datasource.createInitialOptions).toBe('function'); + }); + + it('should create initial options with version and database', () => { + const initialOptions = InfluxDBV1Datasource.createInitialOptions(); + expect(initialOptions).toEqual({ + version: 'v1', + database: '', + }); + }); + + it('should throw error when no URL is provided', () => { + const spec = { + directUrl: undefined, + }; + const options = { + proxyUrl: undefined, + }; + + expect(() => { + InfluxDBV1Datasource.createClient(spec, options); + }).toThrow('No URL specified for InfluxDB v1.8 client'); + }); + + it('should use directUrl when provided', () => { + const spec = { + directUrl: 'http://localhost:8086', + }; + const options = { + proxyUrl: 'http://proxy:8086', + }; + + const client = InfluxDBV1Datasource.createClient(spec, options); + expect(client.options.datasourceUrl).toBe('http://localhost:8086'); + }); + + it('should fall back to proxyUrl when directUrl is not provided', () => { + const spec = { + directUrl: undefined, + }; + const options = { + proxyUrl: 'http://proxy:8086', + }; + + const client = InfluxDBV1Datasource.createClient(spec, options); + expect(client.options.datasourceUrl).toBe('http://proxy:8086'); + }); + + it('should throw error on v3 queries', async () => { + const spec = { + directUrl: 'http://localhost:8086', + }; + const options = { + proxyUrl: undefined, + }; + + const client = InfluxDBV1Datasource.createClient(spec, options); + await expect(client.queryV3SQL()).rejects.toThrow('InfluxDB v3 queries not supported on v1.8 datasource'); + }); +}); + diff --git a/influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts b/influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts new file mode 100644 index 000000000..1bc7295d8 --- /dev/null +++ b/influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts @@ -0,0 +1,107 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { getPluginModule } from '../getPluginModule'; +import packageJson from '../../package.json'; + +describe('InfluxDB Plugin Integration', () => { + describe('Plugin Module Resource', () => { + it('should return valid PluginModuleResource', () => { + const pluginModule = getPluginModule(); + expect(pluginModule.kind).toBe('PluginModule'); + }); + + it('should have correct metadata from package.json', () => { + const pluginModule = getPluginModule(); + expect(pluginModule.metadata.name).toBe(packageJson.name); + expect(pluginModule.metadata.version).toBe(packageJson.version); + }); + + it('should include all plugins from package.json perses config', () => { + const pluginModule = getPluginModule(); + const expectedPlugins = packageJson.perses.plugins; + expect(pluginModule.spec.plugins).toEqual(expectedPlugins); + }); + + it('should have correct plugin kinds', () => { + const pluginModule = getPluginModule(); + const pluginKinds = pluginModule.spec.plugins.map((p: any) => p.kind); + expect(pluginKinds).toContain('Datasource'); + expect(pluginKinds).toContain('TimeSeriesQuery'); + }); + + it('should have correct plugin names', () => { + const pluginModule = getPluginModule(); + const pluginNames = pluginModule.spec.plugins.map((p: any) => p.spec.name); + expect(pluginNames).toContain('InfluxDBDatasource'); + expect(pluginNames).toContain('InfluxDBTimeSeriesQuery'); + }); + }); + + describe('Plugin Metadata', () => { + it('should have display names for all plugins', () => { + const pluginModule = getPluginModule(); + pluginModule.spec.plugins.forEach((plugin: any) => { + expect(plugin.spec.display).toBeDefined(); + expect(plugin.spec.display.name).toBeDefined(); + expect(plugin.spec.display.name).toBeTruthy(); + }); + }); + + it('should have valid plugin specifications', () => { + const pluginModule = getPluginModule(); + pluginModule.spec.plugins.forEach((plugin: any) => { + expect(plugin.kind).toBeTruthy(); + expect(plugin.spec).toBeDefined(); + expect(plugin.spec.name).toBeDefined(); + }); + }); + }); + + describe('Datasource Plugin Configuration', () => { + it('should have Datasource plugin configured', () => { + const pluginModule = getPluginModule(); + const datasourcePlugin = pluginModule.spec.plugins.find( + (p: any) => p.kind === 'Datasource' && p.spec.name === 'InfluxDBDatasource' + ); + expect(datasourcePlugin).toBeDefined(); + }); + + it('should have correct Datasource display name', () => { + const pluginModule = getPluginModule(); + const datasourcePlugin = pluginModule.spec.plugins.find( + (p: any) => p.kind === 'Datasource' && p.spec.name === 'InfluxDBDatasource' + ); + expect(datasourcePlugin.spec.display.name).toBe('InfluxDB Datasource'); + }); + }); + + describe('TimeSeriesQuery Plugin Configuration', () => { + it('should have TimeSeriesQuery plugin configured', () => { + const pluginModule = getPluginModule(); + const queryPlugin = pluginModule.spec.plugins.find( + (p: any) => p.kind === 'TimeSeriesQuery' && p.spec.name === 'InfluxDBTimeSeriesQuery' + ); + expect(queryPlugin).toBeDefined(); + }); + + it('should have correct TimeSeriesQuery display name', () => { + const pluginModule = getPluginModule(); + const queryPlugin = pluginModule.spec.plugins.find( + (p: any) => p.kind === 'TimeSeriesQuery' && p.spec.name === 'InfluxDBTimeSeriesQuery' + ); + expect(queryPlugin.spec.display.name).toBe('InfluxDB Time Series Query'); + }); + }); +}); + diff --git a/influxdb/src/__tests__/ModuleFederationExports.test.ts b/influxdb/src/__tests__/ModuleFederationExports.test.ts new file mode 100644 index 000000000..b40705a43 --- /dev/null +++ b/influxdb/src/__tests__/ModuleFederationExports.test.ts @@ -0,0 +1,84 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as InfluxDBDatasourceModule from '../datasources/influxdb-v1/InfluxDBV1Datasource'; +import * as InfluxDBTimeSeriesQueryModule from '../queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; + +describe('Module Federation Exports', () => { + describe('InfluxDBDatasource Module', () => { + it('should export default InfluxDBV1Datasource', () => { + expect(InfluxDBDatasourceModule.default).toBeDefined(); + expect(InfluxDBDatasourceModule.default).toHaveProperty('createClient'); + }); + + it('should export named InfluxDBV1Datasource', () => { + expect(InfluxDBDatasourceModule.InfluxDBV1Datasource).toBeDefined(); + expect(InfluxDBDatasourceModule.InfluxDBV1Datasource).toEqual(InfluxDBDatasourceModule.default); + }); + + it('should have all required datasource plugin properties', () => { + const datasource = InfluxDBDatasourceModule.default; + expect(datasource).toHaveProperty('createClient'); + expect(datasource).toHaveProperty('createInitialOptions'); + expect(datasource).toHaveProperty('OptionsEditorComponent'); + }); + }); + + describe('InfluxDBTimeSeriesQuery Module', () => { + it('should export default InfluxDBTimeSeriesQuery', () => { + expect(InfluxDBTimeSeriesQueryModule.default).toBeDefined(); + expect(InfluxDBTimeSeriesQueryModule.default).toHaveProperty('getTimeSeriesData'); + }); + + it('should export named InfluxDBTimeSeriesQuery', () => { + expect(InfluxDBTimeSeriesQueryModule.InfluxDBTimeSeriesQuery).toBeDefined(); + expect(InfluxDBTimeSeriesQueryModule.InfluxDBTimeSeriesQuery).toEqual(InfluxDBTimeSeriesQueryModule.default); + }); + + it('should have all required query plugin properties', () => { + const query = InfluxDBTimeSeriesQueryModule.default; + expect(query).toHaveProperty('getTimeSeriesData'); + expect(query).toHaveProperty('createInitialOptions'); + expect(query).toHaveProperty('OptionsEditorComponent'); + }); + }); + + describe('Bootstrap Module', () => { + it('should export all required modules from bootstrap', async () => { + const bootstrap = await import('../bootstrap'); + expect(bootstrap.getPluginModule).toBeDefined(); + expect(bootstrap.InfluxDBDatasource).toBeDefined(); + expect(bootstrap.InfluxDBTimeSeriesQuery).toBeDefined(); + }); + + it('should export correct InfluxDBDatasource from bootstrap', async () => { + const bootstrap = await import('../bootstrap'); + expect(bootstrap.InfluxDBDatasource).toEqual(InfluxDBDatasourceModule.InfluxDBV1Datasource); + }); + + it('should export correct InfluxDBTimeSeriesQuery from bootstrap', async () => { + const bootstrap = await import('../bootstrap'); + expect(bootstrap.InfluxDBTimeSeriesQuery).toEqual(InfluxDBTimeSeriesQueryModule.InfluxDBTimeSeriesQuery); + }); + + it('should return valid PluginModule metadata', async () => { + const bootstrap = await import('../bootstrap'); + const pluginModule = bootstrap.getPluginModule(); + expect(pluginModule.kind).toBe('PluginModule'); + expect(pluginModule.metadata).toHaveProperty('name'); + expect(pluginModule.metadata).toHaveProperty('version'); + expect(pluginModule.spec).toHaveProperty('plugins'); + }); + }); +}); + diff --git a/influxdb/src/bootstrap.ts b/influxdb/src/bootstrap.ts index 399b43eb5..2eb4ec250 100644 --- a/influxdb/src/bootstrap.ts +++ b/influxdb/src/bootstrap.ts @@ -11,7 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Re-export all plugin modules for Module Federation export { getPluginModule } from './getPluginModule'; + +// Export Datasource plugin export { InfluxDBV1Datasource as InfluxDBDatasource } from './datasources/influxdb-v1'; +export { default as InfluxDBDatasourceDefault } from './datasources/influxdb-v1/InfluxDBV1Datasource'; + +// Export TimeSeriesQuery plugin export { InfluxDBTimeSeriesQuery } from './queries/influxdb-time-series-query'; +export { default as InfluxDBTimeSeriesQueryDefault } from './queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; diff --git a/influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts b/influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts index e354e6ed3..b8630be66 100644 --- a/influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts +++ b/influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts @@ -41,3 +41,7 @@ export const InfluxDBV1Datasource: DatasourcePlugin ({ version: 'v1', database: '' }), OptionsEditorComponent: InfluxDBV1Editor, }; + +// Default export for Module Federation +export default InfluxDBV1Datasource; + diff --git a/influxdb/src/expose-datasource.ts b/influxdb/src/expose-datasource.ts new file mode 100644 index 000000000..495fefa23 --- /dev/null +++ b/influxdb/src/expose-datasource.ts @@ -0,0 +1,16 @@ +// Copyright The Perses Authors + +export { default } from './datasources/influxdb-v1/InfluxDBV1Datasource'; +// Expose InfluxDBDatasource plugin + +// limitations under the License. +// See the License for the specific language governing permissions and +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// distributed under the License is distributed on an "AS IS" BASIS, +// Unless required by applicable law or agreed to in writing, software +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// You may obtain a copy of the License at +// you may not use this file except in compliance with the License. +// Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/influxdb/src/expose-query.ts b/influxdb/src/expose-query.ts new file mode 100644 index 000000000..ea0751e32 --- /dev/null +++ b/influxdb/src/expose-query.ts @@ -0,0 +1,16 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Expose InfluxDBTimeSeriesQuery plugin +export { default } from './queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; + diff --git a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts index 1241bf66d..ce1fc987a 100644 --- a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts +++ b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts @@ -90,3 +90,7 @@ export const InfluxDBTimeSeriesQuery: TimeSeriesQueryPlugin ({ query: '' }), }; + +// Default export for Module Federation +export default InfluxDBTimeSeriesQuery; + From c224aa91fc4d92508bbfb043218f05fc71b42096 Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Mon, 23 Feb 2026 21:34:15 +0100 Subject: [PATCH 4/8] feat: Add first version of the DS and the query functionality Signed-off-by: Pascal Zimmermann --- influxdb/package.json | 17 +- influxdb/rsbuild.config.ts | 5 +- .../influxdb.cue} | 33 +-- .../examples/influxdb-v1-direct.yaml | 11 - .../examples/influxdb-v1-proxy.yaml | 22 -- .../examples/influxdb-v3-direct.yaml | 12 - .../examples/influxdb-v3-proxy.yaml | 21 -- .../influx-db-v1-datasource.json | 8 - .../influxdb-time-series-query/query.cue} | 16 +- influxdb/sdk/go/datasource/datasource.go | 4 +- influxdb/sdk/go/datasource/datasource_test.go | 6 +- influxdb/sdk/go/datasource/options.go | 2 +- influxdb/src/bootstrap.ts | 14 +- .../datasource/influxdb/InfluxDBDatasource.ts | 183 +++++++++++++++ .../datasource/influxdb/InfluxDBEditor.tsx | 212 ++++++++++++++++++ influxdb/src/datasource/influxdb/index.ts | 2 + .../influxdb-v1/InfluxDBV1Datasource.ts | 47 ---- .../influxdb-v1/InfluxDBV1Editor.tsx | 38 ---- influxdb/src/datasources/influxdb-v1/index.ts | 15 -- .../influxdb-v3/InfluxDBV3Datasource.ts | 47 ---- .../influxdb-v3/InfluxDBV3Editor.tsx | 47 ---- influxdb/src/datasources/influxdb-v3/index.ts | 15 -- influxdb/src/explore/InfluxDBExplorer.tsx | 83 +++++++ influxdb/src/{model => explore}/index.ts | 3 +- influxdb/src/expose-datasource.ts | 16 -- influxdb/src/index.ts | 22 +- influxdb/src/model/influxdb-types.ts | 59 ----- .../InfluxDBTimeSeriesQuery.ts | 116 ++++++---- .../InfluxDBTimeSeriesQueryEditor.tsx | 21 +- package.json | 1 + 30 files changed, 655 insertions(+), 443 deletions(-) rename influxdb/schemas/{datasources/influx-db-datasource/datasource.cue => datasource/influxdb.cue} (66%) delete mode 100644 influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-direct.yaml delete mode 100644 influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-proxy.yaml delete mode 100644 influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-direct.yaml delete mode 100644 influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-proxy.yaml delete mode 100644 influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.json rename influxdb/{src/expose-query.ts => schemas/influxdb-time-series-query/query.cue} (68%) create mode 100644 influxdb/src/datasource/influxdb/InfluxDBDatasource.ts create mode 100644 influxdb/src/datasource/influxdb/InfluxDBEditor.tsx create mode 100644 influxdb/src/datasource/influxdb/index.ts delete mode 100644 influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts delete mode 100644 influxdb/src/datasources/influxdb-v1/InfluxDBV1Editor.tsx delete mode 100644 influxdb/src/datasources/influxdb-v1/index.ts delete mode 100644 influxdb/src/datasources/influxdb-v3/InfluxDBV3Datasource.ts delete mode 100644 influxdb/src/datasources/influxdb-v3/InfluxDBV3Editor.tsx delete mode 100644 influxdb/src/datasources/influxdb-v3/index.ts create mode 100644 influxdb/src/explore/InfluxDBExplorer.tsx rename influxdb/src/{model => explore}/index.ts (94%) delete mode 100644 influxdb/src/expose-datasource.ts delete mode 100644 influxdb/src/model/influxdb-types.ts diff --git a/influxdb/package.json b/influxdb/package.json index 31382d1f2..11a86e84d 100644 --- a/influxdb/package.json +++ b/influxdb/package.json @@ -29,11 +29,11 @@ "@emotion/styled": "^11.6.0", "@mui/material": "^5.0.0", "@hookform/resolvers": "^3.2.0", - "@perses-dev/components": "^0.53.0-rc.1", + "@perses-dev/components": "^0.53.0-rc.2", "@perses-dev/core": "^0.53.0-beta.4", - "@perses-dev/dashboards": "^0.53.0-rc.1", - "@perses-dev/explore": "^0.53.0-rc.1", - "@perses-dev/plugin-system": "^0.53.0-rc.1", + "@perses-dev/dashboards": "^0.53.0-rc.2", + "@perses-dev/explore": "^0.53.0-rc.2", + "@perses-dev/plugin-system": "^0.53.0-rc.2", "@tanstack/react-query": "^4.39.1", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", @@ -86,6 +86,15 @@ }, "name": "InfluxDBTimeSeriesQuery" } + }, + { + "kind": "Explore", + "spec": { + "display": { + "name": "InfluxDB Explorer" + }, + "name": "InfluxDBExplorer" + } } ] } diff --git a/influxdb/rsbuild.config.ts b/influxdb/rsbuild.config.ts index 4bd715a63..4d8d22641 100644 --- a/influxdb/rsbuild.config.ts +++ b/influxdb/rsbuild.config.ts @@ -8,8 +8,9 @@ export default createConfigForPlugin({ }, moduleFederation: { exposes: { - './InfluxDBDatasource': './src/expose-datasource.ts', - './InfluxDBTimeSeriesQuery': './src/expose-query.ts', + './InfluxDBDatasource': './src/datasource/influxdb/InfluxDBDatasource.ts', + './InfluxDBTimeSeriesQuery': './src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts', + './InfluxDBExplorer': './src/explore/InfluxDBExplorer.tsx', }, shared: { react: { singleton: true, requiredVersion: false }, diff --git a/influxdb/schemas/datasources/influx-db-datasource/datasource.cue b/influxdb/schemas/datasource/influxdb.cue similarity index 66% rename from influxdb/schemas/datasources/influx-db-datasource/datasource.cue rename to influxdb/schemas/datasource/influxdb.cue index 4af074fbe..09544d9c2 100644 --- a/influxdb/schemas/datasources/influx-db-datasource/datasource.cue +++ b/influxdb/schemas/datasource/influxdb.cue @@ -14,26 +14,25 @@ package model import ( - "github.com/perses/perses/cue/common" - commonProxy "github.com/perses/perses/cue/common/proxy" + "github.com/perses/shared/cue/common" + commonProxy "github.com/perses/shared/cue/common/proxy" ) -kind: "InfluxDBDatasource" +#kind: "InfluxDBDatasource" + +kind: #kind spec: { - #v1Config | #v3Config -} + version: "v1" | "v3" + (#directUrl | #proxy) -#v1Config: { - #directUrl | #proxy - version: "v1" - database: string -} -#v3Config: { - #directUrl | #proxy - version: "v3" - organization: string - bucket: string + // V1 specific fields + database?: string + auth?: string + + // V3 specific fields + organization?: string + bucket?: string } #directUrl: { @@ -42,4 +41,6 @@ spec: { #proxy: { proxy: commonProxy.#HTTPProxy -} \ No newline at end of file +} + +#selector: common.#datasourceSelector & { _kind: #kind } diff --git a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-direct.yaml b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-direct.yaml deleted file mode 100644 index 31dba62ff..000000000 --- a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-direct.yaml +++ /dev/null @@ -1,11 +0,0 @@ -kind: GlobalDatasource -metadata: - name: influxdb-v1-demo -spec: - default: false - plugin: - kind: InfluxDBV1Datasource - spec: - directUrl: http://localhost:8086 - version: v1 - database: mydb diff --git a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-proxy.yaml b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-proxy.yaml deleted file mode 100644 index af399521d..000000000 --- a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v1-proxy.yaml +++ /dev/null @@ -1,22 +0,0 @@ -kind: GlobalDatasource -metadata: - name: influxdb-v1-proxy -spec: - default: false - plugin: - kind: InfluxDBV1Datasource - spec: - version: v1 - database: mydb - proxy: - kind: HTTPProxy - spec: - url: http://localhost:8086 - allowedEndpoints: - - endpointPattern: /query - method: GET - - endpointPattern: /query - method: POST - - endpointPattern: /write - method: POST - secret: influxdb-secret diff --git a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-direct.yaml b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-direct.yaml deleted file mode 100644 index f47cfc936..000000000 --- a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-direct.yaml +++ /dev/null @@ -1,12 +0,0 @@ -kind: GlobalDatasource -metadata: - name: influxdb-v3-demo -spec: - default: false - plugin: - kind: InfluxDBV3Datasource - spec: - directUrl: http://localhost:8086 - version: v3 - organization: myorg - bucket: mybucket diff --git a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-proxy.yaml b/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-proxy.yaml deleted file mode 100644 index 185611cf6..000000000 --- a/influxdb/schemas/datasources/influx-db-datasource/examples/influxdb-v3-proxy.yaml +++ /dev/null @@ -1,21 +0,0 @@ -kind: GlobalDatasource -metadata: - name: influxdb-v3-proxy -spec: - default: false - plugin: - kind: InfluxDBV3Datasource - spec: - version: v3 - organization: myorg - bucket: mybucket - proxy: - kind: HTTPProxy - spec: - url: http://localhost:8086 - allowedEndpoints: - - endpointPattern: /api/v3/query_sql - method: POST - - endpointPattern: /api/v3/query_influxql - method: POST - secret: influxdb-secret diff --git a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.json b/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.json deleted file mode 100644 index 3100cacbe..000000000 --- a/influxdb/schemas/datasources/influx-db-datasource/influx-db-v1-datasource.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "kind": "InfluxDBV1Datasource", - "spec": { - "directUrl": "http://localhost:8086", - "version": "v1", - "database": "mydb" - } -} \ No newline at end of file diff --git a/influxdb/src/expose-query.ts b/influxdb/schemas/influxdb-time-series-query/query.cue similarity index 68% rename from influxdb/src/expose-query.ts rename to influxdb/schemas/influxdb-time-series-query/query.cue index ea0751e32..39b85facb 100644 --- a/influxdb/src/expose-query.ts +++ b/influxdb/schemas/influxdb-time-series-query/query.cue @@ -11,6 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Expose InfluxDBTimeSeriesQuery plugin -export { default } from './queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; +package model +import ( + "strings" + ds "github.com/perses/plugins/influxdb/schemas/datasource:model" +) + +kind: "InfluxDBTimeSeriesQuery" +spec: close({ + ds.#selector + query: strings.MinRunes(1) + seriesNameFormat?: string +}) + +#variableSyntaxRegex: "^\\$\\w+$" diff --git a/influxdb/sdk/go/datasource/datasource.go b/influxdb/sdk/go/datasource/datasource.go index 120540e12..3eada9d14 100644 --- a/influxdb/sdk/go/datasource/datasource.go +++ b/influxdb/sdk/go/datasource/datasource.go @@ -38,7 +38,7 @@ type PluginSpec struct { DirectURL string `json:"directUrl,omitempty" yaml:"directUrl,omitempty"` Proxy *http.Proxy `json:"proxy,omitempty" yaml:"proxy,omitempty"` Version InfluxDBVersion `json:"version" yaml:"version"` - // V1.8 specific fields + // V1 specific fields Database string `json:"database,omitempty" yaml:"database,omitempty"` // V3 specific fields Organization string `json:"organization,omitempty" yaml:"organization,omitempty"` @@ -82,7 +82,7 @@ func (s *PluginSpec) validate() error { return fmt.Errorf("version must be either 'v1' or 'v3'") } if s.Version == VersionV1 && len(s.Database) == 0 { - return fmt.Errorf("database is required for InfluxDB v1.8") + return fmt.Errorf("database is required for InfluxDB v1") } if s.Version == VersionV3 { if len(s.Organization) == 0 { diff --git a/influxdb/sdk/go/datasource/datasource_test.go b/influxdb/sdk/go/datasource/datasource_test.go index 438ef384d..6b891f518 100644 --- a/influxdb/sdk/go/datasource/datasource_test.go +++ b/influxdb/sdk/go/datasource/datasource_test.go @@ -29,7 +29,7 @@ func TestPluginSpecValidation(t *testing.T) { errorMsg string }{ { - name: "valid V1.8 with directUrl", + name: "valid V1 with directUrl", spec: PluginSpec{ DirectURL: "http://localhost:8086", Version: VersionV1, @@ -82,7 +82,7 @@ func TestPluginSpecValidation(t *testing.T) { Version: VersionV1, }, expectError: true, - errorMsg: "database is required for InfluxDB v1.8", + errorMsg: "database is required for InfluxDB v1", }, { name: "V3 missing organization", @@ -129,7 +129,7 @@ func TestPluginSpecUnmarshalJSON(t *testing.T) { expected *PluginSpec }{ { - name: "valid V1.8 JSON", + name: "valid V1 JSON", json: `{ "directUrl": "http://localhost:8086", "version": "v1", diff --git a/influxdb/sdk/go/datasource/options.go b/influxdb/sdk/go/datasource/options.go index e583cb59b..4e9e7d6ad 100644 --- a/influxdb/sdk/go/datasource/options.go +++ b/influxdb/sdk/go/datasource/options.go @@ -41,7 +41,7 @@ func Version(version InfluxDBVersion) Option { } } -// Database sets the database name for InfluxDB V1.8 +// Database sets the database name for InfluxDB V1 func Database(database string) Option { return func(builder *Builder) error { builder.Database = database diff --git a/influxdb/src/bootstrap.ts b/influxdb/src/bootstrap.ts index 2eb4ec250..46787fc9d 100644 --- a/influxdb/src/bootstrap.ts +++ b/influxdb/src/bootstrap.ts @@ -11,14 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Re-export all plugin modules for Module Federation +// Re-export plugin module metadata export { getPluginModule } from './getPluginModule'; -// Export Datasource plugin -export { InfluxDBV1Datasource as InfluxDBDatasource } from './datasources/influxdb-v1'; -export { default as InfluxDBDatasourceDefault } from './datasources/influxdb-v1/InfluxDBV1Datasource'; +// Re-export the unified datasource for both V1 and V3 +export { InfluxDBDatasource } from './datasource/influxdb/InfluxDBDatasource'; -// Export TimeSeriesQuery plugin -export { InfluxDBTimeSeriesQuery } from './queries/influxdb-time-series-query'; -export { default as InfluxDBTimeSeriesQueryDefault } from './queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; +// Re-export the query with proper name for module federation +export { InfluxDBTimeSeriesQuery } from './queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; +// Re-export the explore plugin +export { InfluxDBExplorer } from './explore/InfluxDBExplorer'; diff --git a/influxdb/src/datasource/influxdb/InfluxDBDatasource.ts b/influxdb/src/datasource/influxdb/InfluxDBDatasource.ts new file mode 100644 index 000000000..e0ff615f6 --- /dev/null +++ b/influxdb/src/datasource/influxdb/InfluxDBDatasource.ts @@ -0,0 +1,183 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { fetch, RequestHeaders, HTTPProxy } from '@perses-dev/core'; +import { DatasourcePlugin } from '@perses-dev/plugin-system'; +import { InfluxDBEditor } from './InfluxDBEditor'; + +export type InfluxDBVersion = 'v1' | 'v3'; + +/** + * Validates that a query is read-only (no write operations) + * Blocks: INSERT, WRITE, DELETE, DROP, ALTER, CREATE, etc. + * Allows: SELECT, SHOW, etc. + */ +function validateReadOnlyQuery(query: string): void { + const writePatterns = [ + /^\s*INSERT\s/i, + /^\s*WRITE\s/i, + /^\s*DELETE\s/i, + /^\s*DROP\s/i, + /^\s*ALTER\s/i, + /^\s*CREATE\s/i, + /^\s*TRUNCATE\s/i, + /^\s*UPDATE\s/i, + ]; + + const trimmedQuery = query.trim(); + + for (const pattern of writePatterns) { + if (pattern.test(trimmedQuery)) { + const firstWord = trimmedQuery.split(' ')[0]?.toUpperCase() || 'UNKNOWN'; + throw new Error( + `Write operations are not allowed. Query cannot start with: ${firstWord}. ` + + 'Only read-only queries (SELECT, SHOW, etc.) are permitted.' + ); + } + } +} + +export interface InfluxDBSpec { + version: InfluxDBVersion; + directUrl?: string; + proxy?: HTTPProxy; + // V1 specific + database?: string; + auth?: string; + // V3 specific + organization?: string; + bucket?: string; +} +export interface InfluxDBV1Response { + results: Array<{ + series?: Array<{ + name: string; + columns: string[]; + values: Array>; + }>; + error?: string; + }>; +} +export interface InfluxDBV3Response { + data?: Record; + error?: string; +} +export interface InfluxDBClient { + options: { datasourceUrl: string; headers?: RequestHeaders }; + queryV1?(query: string, database: string, headers?: RequestHeaders): Promise; + queryV3SQL?(query: string, headers?: RequestHeaders): Promise; + queryV3Flux?(query: string, headers?: RequestHeaders): Promise; +} +const createClient: DatasourcePlugin['createClient'] = (spec, options) => { + const { version, directUrl } = spec; + const { proxyUrl } = options; + const datasourceUrl = directUrl ?? proxyUrl; + if (!datasourceUrl) { + throw new Error('No URL specified for InfluxDB client'); + } + // Note: Authentication and TLS are handled by the backend + // - Proxy: backend resolves spec.auth secret and injects headers/certs via HTTPProxy + // - Direct: HTTP client applies spec.auth secret for TLS and Basic Auth + // Backend handles: password (v1), token (v3), TLS certs, CA certs, proxy headers + const authHeaders: RequestHeaders = {}; + const fetchOptions: RequestInit = { headers: authHeaders }; + const client: InfluxDBClient = { + options: { datasourceUrl, headers: authHeaders }, + }; + if (version === 'v1') { + client.queryV1 = async (query: string, database: string, headers?: RequestHeaders): Promise => { + if (!spec.database && !database) { + throw new Error('Database is required for InfluxDB v1'); + } + // V1 Proxy Mode: Only supports read-only /query endpoint (GET) + // Write operations via /write endpoint are NOT allowed in proxy mode + const url = new URL('/query', datasourceUrl); + url.searchParams.set('db', database || spec.database!); + url.searchParams.set('q', query); + const response = await fetch(url.toString(), { + ...fetchOptions, + method: 'GET', + headers: { ...authHeaders, ...headers }, + }); + if (!response.ok) throw new Error('InfluxDB v1 query failed: ' + response.statusText); + return response.json(); + }; + } else if (version === 'v3') { + client.queryV3SQL = async (query: string, headers?: RequestHeaders): Promise => { + // Validate: Only read-only queries allowed (no INSERT, WRITE, DELETE, etc.) + validateReadOnlyQuery(query); + + // V3 SQL Read-Only: /api/v2/query endpoint + // Only read-only queries are allowed in both direct and proxy access + const url = new URL('/api/v2/query', datasourceUrl); + const body = JSON.stringify({ + query, + dialect: { + header: true, + delimiter: ',', + quoteChar: '"', + commentPrefix: '#', + dateTimeFormat: 'RFC3339', + annotations: ['datatype', 'group', 'default'], + }, + }); + const response = await fetch(url.toString(), { + ...fetchOptions, + method: 'POST', + headers: { ...authHeaders, ...headers }, + body, + }); + if (!response.ok) { + throw new Error(`InfluxDB v3 query failed: ${response.statusText}`); + } + return response.json(); + }; + client.queryV3Flux = async (query: string, headers?: RequestHeaders): Promise => { + if (!spec.organization) { + throw new Error('Organization is required for InfluxDB v3 Flux'); + } + // Validate: Only read-only queries allowed (no INSERT, WRITE, DELETE, etc.) + validateReadOnlyQuery(query); + + // V3 Flux Read-Only: /api/v2/query endpoint + // Only read-only queries are allowed in both direct and proxy access + const url = new URL('/api/v2/query', datasourceUrl); + const body = new URLSearchParams({ + org: spec.organization, + query, + }); + const response = await fetch(url.toString(), { + ...fetchOptions, + method: 'POST', + headers: { ...authHeaders, ...headers }, + body, + }); + if (!response.ok) { + throw new Error(`InfluxDB v3 flux query failed: ${response.statusText}`); + } + return response.json(); + }; + } + return client; +}; +export const InfluxDBDatasource: DatasourcePlugin = { + createClient, + createInitialOptions: () => ({ + version: 'v1', + database: '', + }), + OptionsEditorComponent: InfluxDBEditor, +}; +// Named export with the name expected by module federation +export const InfluxDBDatasourceExport = InfluxDBDatasource; +// Default export for Module Federation +export default InfluxDBDatasource; diff --git a/influxdb/src/datasource/influxdb/InfluxDBEditor.tsx b/influxdb/src/datasource/influxdb/InfluxDBEditor.tsx new file mode 100644 index 000000000..0f286dc94 --- /dev/null +++ b/influxdb/src/datasource/influxdb/InfluxDBEditor.tsx @@ -0,0 +1,212 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { + TextField, + Box, + Stack, + FormControl, + InputLabel, + Select, + MenuItem, + Typography, +} from '@mui/material'; +import { HTTPSettingsEditor } from '@perses-dev/plugin-system'; +import { HTTPDatasourceSpec } from '@perses-dev/core'; +import React, { ReactElement } from 'react'; +import { InfluxDBSpec } from './InfluxDBDatasource'; +export interface InfluxDBEditorProps { + value: InfluxDBSpec; + onChange: (next: InfluxDBSpec) => void; + isReadonly?: boolean; +} +export function InfluxDBEditor(props: InfluxDBEditorProps): ReactElement { + const { value, onChange, isReadonly } = props; + const version = value?.version || 'v1'; + // Version-specific AllowedEndpoints + const allowedEndpointsV1 = [ + { + endpointPattern: '/query', + method: 'GET', + }, + ]; + const allowedEndpointsV3 = [ + { + endpointPattern: '/api/v3/query_sql', + method: 'GET', + }, + { + endpointPattern: '/api/v3/query_influxql', + method: 'GET', + }, + ]; + // Build initial specs with version-specific endpoints + const initialSpecDirectV1: HTTPDatasourceSpec = { + directUrl: '', + }; + const initialSpecProxyV1: HTTPDatasourceSpec = { + proxy: { + kind: 'HTTPProxy', + spec: { + allowedEndpoints: allowedEndpointsV1, + url: '', + }, + }, + }; + const initialSpecDirectV3: HTTPDatasourceSpec = { + directUrl: '', + }; + const initialSpecProxyV3: HTTPDatasourceSpec = { + proxy: { + kind: 'HTTPProxy', + spec: { + allowedEndpoints: allowedEndpointsV3, + url: '', + }, + }, + }; + // Select the right initial specs based on current version + const initialSpecDirect = version === 'v1' ? initialSpecDirectV1 : initialSpecDirectV3; + const initialSpecProxy = version === 'v1' ? initialSpecProxyV1 : initialSpecProxyV3; + // Handle changes from HTTP Settings + const handleHTTPSettingsChange = (next: any) => { + const updated = next as InfluxDBSpec; + // If this is a proxy config, ensure it has the correct version-specific endpoints + if (updated.proxy?.spec) { + const correctEndpoints = version === 'v1' ? allowedEndpointsV1 : allowedEndpointsV3; + onChange({ + ...updated, + proxy: { + ...updated.proxy, + spec: { + ...updated.proxy.spec, + allowedEndpoints: correctEndpoints, + }, + }, + }); + } else { + onChange(updated); + } + }; + return ( + + {/* 1. Version Selection FIRST */} + + + Version + + + + {/* 2. HTTP Settings (Direct/Proxy) with version-specific endpoints */} + + {/* 3. Version-Specific Fields */} + {version === 'v1' && ( + + + InfluxDB v1 Configuration + + + onChange({ ...value, database: e.target.value })} + placeholder="e.g., mydb" + disabled={isReadonly} + fullWidth + helperText="Name of the InfluxDB database" + /> + onChange({ ...value, auth: e.target.value })} + placeholder="Secret name containing credentials" + disabled={isReadonly} + fullWidth + helperText="Name of the secret containing username/password" + /> + + + )} + {version === 'v3' && ( + + + InfluxDB v3 Configuration + + + onChange({ ...value, organization: e.target.value })} + placeholder="e.g., myorg" + disabled={isReadonly} + fullWidth + helperText="Name of the InfluxDB organization" + /> + onChange({ ...value, bucket: e.target.value })} + placeholder="e.g., mybucket" + disabled={isReadonly} + fullWidth + helperText="Name of the InfluxDB bucket" + /> + onChange({ ...value, auth: e.target.value })} + placeholder="Secret name containing token" + disabled={isReadonly} + fullWidth + helperText="Name of the secret containing auth token" + /> + + + )} + + ); +} diff --git a/influxdb/src/datasource/influxdb/index.ts b/influxdb/src/datasource/influxdb/index.ts new file mode 100644 index 000000000..2dece5efb --- /dev/null +++ b/influxdb/src/datasource/influxdb/index.ts @@ -0,0 +1,2 @@ +export * from './InfluxDBDatasource'; +export * from './InfluxDBEditor'; diff --git a/influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts b/influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts deleted file mode 100644 index b8630be66..000000000 --- a/influxdb/src/datasources/influxdb-v1/InfluxDBV1Datasource.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -import { fetch, RequestHeaders } from '@perses-dev/core'; -import { DatasourcePlugin } from '@perses-dev/plugin-system'; -import { InfluxDBV1Spec, InfluxDBClient, InfluxDBV1Response, InfluxDBV3Response } from '../../model'; -import { InfluxDBV1Editor } from './InfluxDBV1Editor'; -const createClient: DatasourcePlugin['createClient'] = (spec, options) => { - const { directUrl } = spec; - const { proxyUrl } = options; - const datasourceUrl = directUrl ?? proxyUrl; - if (!datasourceUrl) { - throw new Error('No URL specified for InfluxDB v1.8 client'); - } - return { - options: { datasourceUrl }, - queryV1: async (query: string, database: string, headers?: RequestHeaders): Promise => { - const url = new URL('/query', datasourceUrl); - url.searchParams.set('db', database); - url.searchParams.set('q', query); - const response = await fetch(url.toString(), { method: 'GET', headers }); - if (!response.ok) throw new Error('InfluxDB v1.8 query failed: ' + response.statusText); - return response.json(); - }, - queryV3SQL: async (): Promise => { - throw new Error('InfluxDB v3 queries not supported on v1.8 datasource'); - }, - }; -}; -export const InfluxDBV1Datasource: DatasourcePlugin = { - createClient, - createInitialOptions: () => ({ version: 'v1', database: '' }), - OptionsEditorComponent: InfluxDBV1Editor, -}; - -// Default export for Module Federation -export default InfluxDBV1Datasource; - diff --git a/influxdb/src/datasources/influxdb-v1/InfluxDBV1Editor.tsx b/influxdb/src/datasources/influxdb-v1/InfluxDBV1Editor.tsx deleted file mode 100644 index 74fc122c6..000000000 --- a/influxdb/src/datasources/influxdb-v1/InfluxDBV1Editor.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -import { TextField, Box } from '@mui/material'; -import { OptionsEditorProps } from '@perses-dev/plugin-system'; -import { InfluxDBV1Spec } from '../../model'; -export function InfluxDBV1Editor({ value, onChange }: OptionsEditorProps) { - return ( - - onChange({ ...value, directUrl: e.target.value })} - helperText="Optional: URL to access InfluxDB directly from the browser" - fullWidth - /> - onChange({ ...value, database: e.target.value })} - helperText="The InfluxDB database to query" - required - fullWidth - /> - - ); -} diff --git a/influxdb/src/datasources/influxdb-v1/index.ts b/influxdb/src/datasources/influxdb-v1/index.ts deleted file mode 100644 index fc5eff284..000000000 --- a/influxdb/src/datasources/influxdb-v1/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -export * from './InfluxDBV1Datasource'; -export * from './InfluxDBV1Editor'; diff --git a/influxdb/src/datasources/influxdb-v3/InfluxDBV3Datasource.ts b/influxdb/src/datasources/influxdb-v3/InfluxDBV3Datasource.ts deleted file mode 100644 index bac3fd662..000000000 --- a/influxdb/src/datasources/influxdb-v3/InfluxDBV3Datasource.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { fetch, RequestHeaders } from '@perses-dev/core'; -import { DatasourcePlugin } from '@perses-dev/plugin-system'; -import { InfluxDBV3Spec, InfluxDBClient, InfluxDBV1Response, InfluxDBV3Response } from '../../model'; -import { InfluxDBV3Editor } from './InfluxDBV3Editor'; - -const createClient: DatasourcePlugin['createClient'] = (spec, options) => { - const { directUrl } = spec; - const { proxyUrl } = options; - const datasourceUrl = directUrl ?? proxyUrl; - if (!datasourceUrl) { - throw new Error('No URL specified for InfluxDB v3 client'); - } - return { - options: { datasourceUrl }, - queryV1: async (): Promise => { - throw new Error('InfluxDB v1.8 queries not supported on v3 datasource'); - }, - queryV3SQL: async (query: string, organization: string, bucket: string, headers?: RequestHeaders): Promise => { - const url = new URL('/api/v3/query_sql', datasourceUrl); - const response = await fetch(url.toString(), { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...headers }, - body: JSON.stringify({ query, query_type: 'sql', params: { organization, bucket } }), - }); - if (!response.ok) throw new Error('InfluxDB v3 query failed: ' + response.statusText); - return response.json(); - }, - }; -}; -export const InfluxDBV3Datasource: DatasourcePlugin = { - createClient, - createInitialOptions: () => ({ version: 'v3', organization: '', bucket: '' }), - OptionsEditorComponent: InfluxDBV3Editor, -}; diff --git a/influxdb/src/datasources/influxdb-v3/InfluxDBV3Editor.tsx b/influxdb/src/datasources/influxdb-v3/InfluxDBV3Editor.tsx deleted file mode 100644 index 16f619104..000000000 --- a/influxdb/src/datasources/influxdb-v3/InfluxDBV3Editor.tsx +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -import { TextField, Box } from '@mui/material'; -import { OptionsEditorProps } from '@perses-dev/plugin-system'; -import { InfluxDBV3Spec } from '../../model'; -export function InfluxDBV3Editor({ value, onChange }: OptionsEditorProps) { - return ( - - onChange({ ...value, directUrl: e.target.value })} - helperText="Optional: URL to access InfluxDB directly from the browser" - fullWidth - /> - onChange({ ...value, organization: e.target.value })} - helperText="The InfluxDB organization" - required - fullWidth - /> - onChange({ ...value, bucket: e.target.value })} - helperText="The InfluxDB bucket to query" - required - fullWidth - /> - - ); -} diff --git a/influxdb/src/datasources/influxdb-v3/index.ts b/influxdb/src/datasources/influxdb-v3/index.ts deleted file mode 100644 index 562f7e30d..000000000 --- a/influxdb/src/datasources/influxdb-v3/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -export * from './InfluxDBV3Datasource'; -export * from './InfluxDBV3Editor'; diff --git a/influxdb/src/explore/InfluxDBExplorer.tsx b/influxdb/src/explore/InfluxDBExplorer.tsx new file mode 100644 index 000000000..cf3918519 --- /dev/null +++ b/influxdb/src/explore/InfluxDBExplorer.tsx @@ -0,0 +1,83 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Box, Stack } from '@mui/material'; +import { ReactElement, useState } from 'react'; +import { DataQueriesProvider, MultiQueryEditor } from '@perses-dev/plugin-system'; +import { Panel } from '@perses-dev/dashboards'; +import { QueryDefinition } from '@perses-dev/core'; +import { useExplorerManagerContext } from '@perses-dev/explore'; + +interface TimeSeriesExplorerQueryParams { + queries?: QueryDefinition[]; +} + +const initialSpec = { + yAxis: { + label: { + show: true, + }, + }, + tooltip: { + mode: 'multi', + }, +}; + +function TimeSeriesPanel({ queries }: { queries: QueryDefinition[] }): ReactElement { + return ( + + ); +} + +export function InfluxDBExplorer(): ReactElement { + const { + data: { queries = [] }, + setData, + } = useExplorerManagerContext(); + + const [queryDefinitions, setQueryDefinitions] = useState(queries); + + // map QueryDefinition to Definition + const definitions = queries.length + ? queries.map((query: QueryDefinition) => { + return { + kind: query.spec.plugin.kind, + spec: query.spec.plugin.spec, + }; + }) + : []; + + return ( + + setQueryDefinitions(state)} + queries={queryDefinitions} + onQueryRun={() => setData({ queries: queryDefinitions })} + /> + + + + + + + ); +} diff --git a/influxdb/src/model/index.ts b/influxdb/src/explore/index.ts similarity index 94% rename from influxdb/src/model/index.ts rename to influxdb/src/explore/index.ts index 8f24c261d..23ad50f76 100644 --- a/influxdb/src/model/index.ts +++ b/influxdb/src/explore/index.ts @@ -10,4 +10,5 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -export * from './influxdb-types'; + +export * from './InfluxDBExplorer'; diff --git a/influxdb/src/expose-datasource.ts b/influxdb/src/expose-datasource.ts deleted file mode 100644 index 495fefa23..000000000 --- a/influxdb/src/expose-datasource.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright The Perses Authors - -export { default } from './datasources/influxdb-v1/InfluxDBV1Datasource'; -// Expose InfluxDBDatasource plugin - -// limitations under the License. -// See the License for the specific language governing permissions and -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// distributed under the License is distributed on an "AS IS" BASIS, -// Unless required by applicable law or agreed to in writing, software -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// You may obtain a copy of the License at -// you may not use this file except in compliance with the License. -// Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/influxdb/src/index.ts b/influxdb/src/index.ts index 4f0fd2bb5..e68903ca9 100644 --- a/influxdb/src/index.ts +++ b/influxdb/src/index.ts @@ -1,4 +1,18 @@ -export * from './datasources/influxdb-v1'; -export * from './datasources/influxdb-v3'; -export * from './queries/influxdb-time-series-query'; -export * from './model'; +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export * from './explore'; +export { getPluginModule } from './getPluginModule'; +export * from './datasource/influxdb/InfluxDBDatasource'; +export * from './queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; + diff --git a/influxdb/src/model/influxdb-types.ts b/influxdb/src/model/influxdb-types.ts deleted file mode 100644 index d4d4a19d5..000000000 --- a/influxdb/src/model/influxdb-types.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -import { HTTPProxy, RequestHeaders } from '@perses-dev/core'; -import { DatasourceClient } from '@perses-dev/plugin-system'; -export type InfluxDBVersion = 'v1' | 'v3'; -export interface InfluxDBV1Spec { - directUrl?: string; - proxy?: HTTPProxy; - version: 'v1'; - database: string; -} -export interface InfluxDBV3Spec { - directUrl?: string; - proxy?: HTTPProxy; - version: 'v3'; - organization: string; - bucket: string; -} -export type InfluxDBDatasourceSpec = InfluxDBV1Spec | InfluxDBV3Spec; -interface InfluxDBClientOptions { - datasourceUrl: string; - headers?: RequestHeaders; -} -export interface InfluxDBV1Response { - results: Array<{ - statement_id?: number; - series?: Array<{ - name: string; - columns: string[]; - values: any[][]; - tags?: Record; - }>; - error?: string; - }>; -} -export interface InfluxDBV3Response { - schema: { - fields: Array<{ - name: string; - data_type: string; - }>; - }; - data: any[][]; -} -export interface InfluxDBClient extends DatasourceClient { - options: InfluxDBClientOptions; - queryV1(query: string, database: string, headers?: RequestHeaders): Promise; - queryV3SQL(query: string, organization: string, bucket: string, headers?: RequestHeaders): Promise; -} diff --git a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts index ce1fc987a..673587c7f 100644 --- a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts +++ b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts @@ -10,10 +10,11 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { TimeSeriesQueryPlugin } from '@perses-dev/plugin-system'; -import { TimeSeriesData, TimeSeries } from '@perses-dev/core'; +import { TimeSeriesQueryPlugin, replaceVariables } from '@perses-dev/plugin-system'; +import { TimeSeriesData, TimeSeries, DatasourceSelector } from '@perses-dev/core'; import { InfluxDBTimeSeriesQueryEditor } from './InfluxDBTimeSeriesQueryEditor'; export interface InfluxDBTimeSeriesQuerySpec { + datasource?: DatasourceSelector; query: string; } function convertV1ResponseToTimeSeries(response: any): TimeSeries[] { @@ -44,53 +45,88 @@ function convertV1ResponseToTimeSeries(response: any): TimeSeries[] { } function convertV3ResponseToTimeSeries(response: any): TimeSeries[] { const series: TimeSeries[] = []; - if (response.data && response.schema) { - const timeField = response.schema.fields.find((f: any) => f.data_type === 'Timestamp'); - const timeIndex = response.schema.fields.indexOf(timeField); - response.schema.fields.forEach((field: any, index: number) => { - if (field.data_type !== 'Timestamp' && field.data_type !== 'Utf8') { - const values = response.data.map((row: any[]) => [new Date(row[timeIndex]).getTime(), row[index]]); - series.push({ - name: field.name, - values: values as Array<[number, number | null]>, - formattedName: field.name, - }); - } - }); - } + // TODO: Implement V3 response conversion + // V3 CSV/Flux response format return series; } export const InfluxDBTimeSeriesQuery: TimeSeriesQueryPlugin = { getTimeSeriesData: async (spec: InfluxDBTimeSeriesQuerySpec, context: any) => { - const { query } = spec; - const { datasourceClient, datasourceSpec } = context; - if (!datasourceClient) { - throw new Error('No datasource client available'); + // Return empty if query is empty + if (!spec.query) { + return { + series: [], + timeRange: context.timeRange, + stepMs: 30 * 1000, + metadata: { + executedQueryString: '', + }, + } as TimeSeriesData; } - let timeSeries: TimeSeries[] = []; - if (datasourceSpec.version === 'v1') { - const response = await datasourceClient.queryV1(query, datasourceSpec.database); - timeSeries = convertV1ResponseToTimeSeries(response); - } else if (datasourceSpec.version === 'v3') { - const response = await datasourceClient.queryV3SQL(query, datasourceSpec.organization, datasourceSpec.bucket); - timeSeries = convertV3ResponseToTimeSeries(response); - } else { - throw new Error('Unsupported InfluxDB version: ' + (datasourceSpec as any).version); + // Replace variables in query + const query = replaceVariables(spec.query, context.variableState); + try { + // Get datasource client from store + const client = await context.datasourceStore.getDatasourceClient( + spec.datasource ?? { kind: 'InfluxDBDatasource' } + ); + if (!client) { + throw new Error('No datasource client available'); + } + // Get time range + const { start, end } = context.timeRange; + let response: any; + let datasourceSpec: any; + // Try to get datasource spec to determine version + try { + datasourceSpec = await context.datasourceStore.getDatasourceSpec?.( + spec.datasource ?? { kind: 'InfluxDBDatasource' } + ); + } catch (e) { + // Spec not available, we'll try to detect from client methods + } + // Determine version and call appropriate query method + if (typeof client.queryV1 === 'function') { + // V1 Query + const database = datasourceSpec?.database || ''; + response = await client.queryV1(query, database); + } else if (typeof client.queryV3SQL === 'function') { + // V3 SQL Query + response = await client.queryV3SQL(query); + } else if (typeof client.queryV3Flux === 'function') { + // V3 Flux Query (fallback) + response = await client.queryV3Flux(query); + } else { + throw new Error( + 'Datasource client has no query methods (queryV1, queryV3SQL, or queryV3Flux)' + ); + } + // Convert response to timeseries + let timeSeries: TimeSeries[] = []; + if (response) { + // Auto-detect V1 vs V3 based on response structure + if (response.results) { + // V1 response format + timeSeries = convertV1ResponseToTimeSeries(response); + } else if (response.data) { + // V3 response format + timeSeries = convertV3ResponseToTimeSeries(response); + } + } + return { + series: timeSeries, + timeRange: { start, end }, + stepMs: 30 * 1000, + metadata: { + executedQueryString: query, + }, + } as TimeSeriesData; + } catch (error) { + console.error('Error executing InfluxDB query:', error); + throw error; } - - return { - series: timeSeries, - timeRange: context.timeRange, - stepMs: 30 * 1000, - metadata: { - executedQueryString: query, - }, - } as TimeSeriesData; }, OptionsEditorComponent: InfluxDBTimeSeriesQueryEditor, createInitialOptions: () => ({ query: '' }), }; - // Default export for Module Federation export default InfluxDBTimeSeriesQuery; - diff --git a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQueryEditor.tsx b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQueryEditor.tsx index d23de26f1..e7566925c 100644 --- a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQueryEditor.tsx +++ b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQueryEditor.tsx @@ -11,17 +11,32 @@ // See the License for the specific language governing permissions and // limitations under the License. import { TextField, Box } from '@mui/material'; -import { OptionsEditorProps } from '@perses-dev/plugin-system'; +import { DatasourceSelect, DatasourceSelectProps, OptionsEditorProps } from '@perses-dev/plugin-system'; +import { DatasourceSelector } from '@perses-dev/core'; import { InfluxDBTimeSeriesQuerySpec } from './InfluxDBTimeSeriesQuery'; +const DATASOURCE_KIND = 'InfluxDBDatasource'; export function InfluxDBTimeSeriesQueryEditor({ value, onChange }: OptionsEditorProps) { + const handleDatasourceChange: DatasourceSelectProps['onChange'] = (newDatasource) => { + // Cast through unknown to handle type mismatch + const datasourceSelector = newDatasource as unknown as DatasourceSelector; + onChange({ ...value, datasource: datasourceSelector }); + }; + const defaultDatasource: DatasourceSelector = { + kind: DATASOURCE_KIND, + }; return ( - + + onChange({ ...value, query: e.target.value })} - helperText="SQL query for InfluxDB v3 or InfluxQL for v1.8" + helperText="SQL query for InfluxDB v3 or InfluxQL for v1" required fullWidth multiline diff --git a/package.json b/package.json index 323eca6d6..06abc6029 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "gaugechart", "heatmapchart", "histogramchart", + "influxdb", "logstable", "loki", "markdown", From e97472303a9a3cba369d8116bd12f2466f5611c7 Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Mon, 23 Feb 2026 21:35:12 +0100 Subject: [PATCH 5/8] feat: Add unit tests Signed-off-by: Pascal Zimmermann --- influxdb/src/__tests__/ExposeModules.test.ts | 20 ++-- .../src/__tests__/InfluxDBDatasource.test.ts | 18 +-- .../InfluxDBPluginIntegration.test.ts | 2 +- .../__tests__/ModuleFederationExports.test.ts | 9 +- .../src/__tests__/SchemaValidation.test.ts | 109 ++++++++++++++++++ 5 files changed, 129 insertions(+), 29 deletions(-) create mode 100644 influxdb/src/__tests__/SchemaValidation.test.ts diff --git a/influxdb/src/__tests__/ExposeModules.test.ts b/influxdb/src/__tests__/ExposeModules.test.ts index 325083419..09eb43a0e 100644 --- a/influxdb/src/__tests__/ExposeModules.test.ts +++ b/influxdb/src/__tests__/ExposeModules.test.ts @@ -11,32 +11,32 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as InfluxDBDatasourceExpose from '../expose-datasource'; -import * as InfluxDBTimeSeriesQueryExpose from '../expose-query'; +import * as InfluxDBDatasourceModule from '../datasource/influxdb/InfluxDBDatasource'; +import * as InfluxDBTimeSeriesQueryModule from '../queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; describe('Module Federation Expose Modules', () => { - describe('expose-datasource.ts', () => { + describe('InfluxDBDatasource.ts', () => { it('should export default InfluxDBDatasource', () => { - expect(InfluxDBDatasourceExpose.default).toBeDefined(); - expect(InfluxDBDatasourceExpose.default).toHaveProperty('createClient'); + expect(InfluxDBDatasourceModule.default).toBeDefined(); + expect(InfluxDBDatasourceModule.default).toHaveProperty('createClient'); }); it('should have all required datasource properties', () => { - const datasource = InfluxDBDatasourceExpose.default; + const datasource = InfluxDBDatasourceModule.default; expect(datasource).toHaveProperty('createClient'); expect(datasource).toHaveProperty('createInitialOptions'); expect(datasource).toHaveProperty('OptionsEditorComponent'); }); }); - describe('expose-query.ts', () => { + describe('influxdb-time-series-query.ts', () => { it('should export default InfluxDBTimeSeriesQuery', () => { - expect(InfluxDBTimeSeriesQueryExpose.default).toBeDefined(); - expect(InfluxDBTimeSeriesQueryExpose.default).toHaveProperty('getTimeSeriesData'); + expect(InfluxDBTimeSeriesQueryModule.default).toBeDefined(); + expect(InfluxDBTimeSeriesQueryModule.default).toHaveProperty('getTimeSeriesData'); }); it('should have all required query properties', () => { - const query = InfluxDBTimeSeriesQueryExpose.default; + const query = InfluxDBTimeSeriesQueryModule.default; expect(query).toHaveProperty('getTimeSeriesData'); expect(query).toHaveProperty('createInitialOptions'); expect(query).toHaveProperty('OptionsEditorComponent'); diff --git a/influxdb/src/__tests__/InfluxDBDatasource.test.ts b/influxdb/src/__tests__/InfluxDBDatasource.test.ts index 48bfd0c45..aeba924a8 100644 --- a/influxdb/src/__tests__/InfluxDBDatasource.test.ts +++ b/influxdb/src/__tests__/InfluxDBDatasource.test.ts @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { InfluxDBV1Datasource } from '../datasources/influxdb-v1'; +import { InfluxDBV1Datasource } from '../datasource/influxdb-v1/InfluxDBV1Datasource'; describe('InfluxDBDatasource', () => { it('should have createClient method', () => { @@ -28,10 +28,9 @@ describe('InfluxDBDatasource', () => { expect(typeof InfluxDBV1Datasource.createInitialOptions).toBe('function'); }); - it('should create initial options with version and database', () => { + it('should create initial options with database', () => { const initialOptions = InfluxDBV1Datasource.createInitialOptions(); expect(initialOptions).toEqual({ - version: 'v1', database: '', }); }); @@ -46,7 +45,7 @@ describe('InfluxDBDatasource', () => { expect(() => { InfluxDBV1Datasource.createClient(spec, options); - }).toThrow('No URL specified for InfluxDB v1.8 client'); + }).toThrow('No URL specified for InfluxDB v1 client'); }); it('should use directUrl when provided', () => { @@ -73,16 +72,5 @@ describe('InfluxDBDatasource', () => { expect(client.options.datasourceUrl).toBe('http://proxy:8086'); }); - it('should throw error on v3 queries', async () => { - const spec = { - directUrl: 'http://localhost:8086', - }; - const options = { - proxyUrl: undefined, - }; - - const client = InfluxDBV1Datasource.createClient(spec, options); - await expect(client.queryV3SQL()).rejects.toThrow('InfluxDB v3 queries not supported on v1.8 datasource'); - }); }); diff --git a/influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts b/influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts index 1bc7295d8..10c338653 100644 --- a/influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts +++ b/influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts @@ -69,7 +69,7 @@ describe('InfluxDB Plugin Integration', () => { }); describe('Datasource Plugin Configuration', () => { - it('should have Datasource plugin configured', () => { + it('should have InfluxDB Datasource configured', () => { const pluginModule = getPluginModule(); const datasourcePlugin = pluginModule.spec.plugins.find( (p: any) => p.kind === 'Datasource' && p.spec.name === 'InfluxDBDatasource' diff --git a/influxdb/src/__tests__/ModuleFederationExports.test.ts b/influxdb/src/__tests__/ModuleFederationExports.test.ts index b40705a43..aa7b957d6 100644 --- a/influxdb/src/__tests__/ModuleFederationExports.test.ts +++ b/influxdb/src/__tests__/ModuleFederationExports.test.ts @@ -11,11 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as InfluxDBDatasourceModule from '../datasources/influxdb-v1/InfluxDBV1Datasource'; +import * as InfluxDBDatasourceModule from '../datasource/influxdb-v1/InfluxDBV1Datasource'; import * as InfluxDBTimeSeriesQueryModule from '../queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; describe('Module Federation Exports', () => { - describe('InfluxDBDatasource Module', () => { + describe('InfluxDBV1Datasource Module', () => { it('should export default InfluxDBV1Datasource', () => { expect(InfluxDBDatasourceModule.default).toBeDefined(); expect(InfluxDBDatasourceModule.default).toHaveProperty('createClient'); @@ -63,7 +63,10 @@ describe('Module Federation Exports', () => { it('should export correct InfluxDBDatasource from bootstrap', async () => { const bootstrap = await import('../bootstrap'); - expect(bootstrap.InfluxDBDatasource).toEqual(InfluxDBDatasourceModule.InfluxDBV1Datasource); + const datasource = bootstrap.InfluxDBDatasource; + expect(datasource).toHaveProperty('createClient'); + expect(datasource).toHaveProperty('createInitialOptions'); + expect(datasource).toHaveProperty('OptionsEditorComponent'); }); it('should export correct InfluxDBTimeSeriesQuery from bootstrap', async () => { diff --git a/influxdb/src/__tests__/SchemaValidation.test.ts b/influxdb/src/__tests__/SchemaValidation.test.ts new file mode 100644 index 000000000..910214f83 --- /dev/null +++ b/influxdb/src/__tests__/SchemaValidation.test.ts @@ -0,0 +1,109 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as fs from 'fs'; +import * as path from 'path'; + +describe('Plugin Schema Validation', () => { + describe('Datasource CUE Schema', () => { + it('should have correct CUE schema file', () => { + const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); + expect(fs.existsSync(schemaPath)).toBe(true); + }); + + it('should contain #kind definition', () => { + const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); + const content = fs.readFileSync(schemaPath, 'utf-8'); + expect(content).toContain('#kind: "InfluxDBDatasource"'); + }); + + it('should contain kind assignment', () => { + const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); + const content = fs.readFileSync(schemaPath, 'utf-8'); + expect(content).toContain('kind: #kind'); + }); + + it('should contain #selector definition', () => { + const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); + const content = fs.readFileSync(schemaPath, 'utf-8'); + expect(content).toContain('#selector'); + expect(content).toContain('common.#datasourceSelector'); + expect(content).toContain('_kind: #kind'); + }); + + it('should have valid unified v1 and v3 configuration', () => { + const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); + const content = fs.readFileSync(schemaPath, 'utf-8'); + // Version selector + expect(content).toContain('version:'); + // Connection type (proxy or direct) + expect(content).toContain('#directUrl'); + expect(content).toContain('#proxy'); + // Auth via secret reference + expect(content).toContain('auth?: string'); + // V1 specific fields + expect(content).toContain('database?: string'); + // V3 specific fields + expect(content).toContain('organization?: string'); + expect(content).toContain('bucket?: string'); + // Import common proxy patterns + expect(content).toContain('commonProxy'); + }); + }); + + describe('Plugin Registration', () => { + it('should have InfluxDBDatasource in package.json', () => { + const packageJsonPath = path.join(__dirname, '../../package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + expect(packageJson.perses).toBeDefined(); + expect(packageJson.perses.plugins).toBeDefined(); + + const datasourcePlugin = packageJson.perses.plugins.find( + (p: any) => p.kind === 'Datasource' && p.spec.name === 'InfluxDBDatasource' + ); + expect(datasourcePlugin).toBeDefined(); + }); + + it('should have InfluxDBTimeSeriesQuery in package.json', () => { + const packageJsonPath = path.join(__dirname, '../../package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + + const queryPlugin = packageJson.perses.plugins.find( + (p: any) => p.kind === 'TimeSeriesQuery' && p.spec.name === 'InfluxDBTimeSeriesQuery' + ); + expect(queryPlugin).toBeDefined(); + }); + }); + + describe('Plugin Module Files', () => { + it('InfluxDBDatasource.ts should have valid TypeScript', () => { + const datasourcePath = path.join(__dirname, '../datasources/influxdb/InfluxDBDatasource.ts'); + const content = fs.readFileSync(datasourcePath, 'utf-8'); + expect(content).toContain('export const InfluxDBDatasource'); + expect(content).toContain('version'); + expect(content).toContain('queryV1'); + expect(content).toContain('queryV3SQL'); + expect(content).toContain('createClient'); + }); + + it('InfluxDBTimeSeriesQuery.ts should have valid TypeScript', () => { + const queryPath = path.join(__dirname, '../queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts'); + const content = fs.readFileSync(queryPath, 'utf-8'); + expect(content).toContain('export const InfluxDBTimeSeriesQuery'); + expect(content).toContain('export default InfluxDBTimeSeriesQuery'); + expect(content).toContain('getTimeSeriesData'); + expect(content).toContain('OptionsEditorComponent'); + }); + }); +}); + From 01c3e33d802b5bbcf5ded3da106977af4c7a30a6 Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Mon, 23 Feb 2026 21:35:45 +0100 Subject: [PATCH 6/8] feat: Add test data utility Signed-off-by: Pascal Zimmermann --- influxdb/test-data/README.md | 253 +++++++++++++++++ .../docker-compose.influxdb-test.yml | 54 ++++ .../test-data/generate-influxdb-testdata.py | 259 ++++++++++++++++++ .../test-data/influxdb-v1/influxdb-v1-init.sh | 55 ++++ .../test-data/influxdb-v3/influxdb-v3-init.sh | 84 ++++++ influxdb/test-data/start-influxdb-testenv.sh | 161 +++++++++++ 6 files changed, 866 insertions(+) create mode 100644 influxdb/test-data/README.md create mode 100644 influxdb/test-data/docker-compose.influxdb-test.yml create mode 100755 influxdb/test-data/generate-influxdb-testdata.py create mode 100755 influxdb/test-data/influxdb-v1/influxdb-v1-init.sh create mode 100644 influxdb/test-data/influxdb-v3/influxdb-v3-init.sh create mode 100755 influxdb/test-data/start-influxdb-testenv.sh diff --git a/influxdb/test-data/README.md b/influxdb/test-data/README.md new file mode 100644 index 000000000..b075e3975 --- /dev/null +++ b/influxdb/test-data/README.md @@ -0,0 +1,253 @@ +# InfluxDB Plugin Test Data Setup + +This directory contains tools to set up a complete InfluxDB testing environment for the Perses InfluxDB plugin. + +## Contents + +- **generate-influxdb-testdata.py** - Python script to generate realistic test data using InfluxDB HTTP API +- **start-influxdb-testenv.sh** - Shell script to start InfluxDB containers +- **docker-compose.influxdb-test.yml** - Docker Compose configuration for InfluxDB V1 and V3 +- **influxdb-v1/influxdb-v1-init.sh** - Initialization script for InfluxDB V1 +- **influxdb-v3/influxdb-v3-init.sh** - Initialization script for InfluxDB V3 + +## Quick Start + +### 1. Start InfluxDB Services + +```bash +./start-influxdb-testenv.sh +``` + +This will: +- Start InfluxDB V1 (port 8086) +- Start InfluxDB V3 (port 8087) +- Create test databases +- Wait for services to be healthy + +### 2. Generate Test Data + +```bash +python3 generate-influxdb-testdata.py +``` + +This generates realistic time-series data: +- **CPU metrics** - 4 hosts × 4 cores × 120 minutes +- **Memory metrics** - 4 hosts × 120 minutes +- **Disk metrics** - 4 hosts × 3 devices × 120 minutes +- **Network metrics** - 4 hosts × 2 interfaces × 120 minutes +- **Temperature metrics** - 4 hosts × 120 minutes + +Total: ~50,000 data points covering the last 2 hours + +### 3. Configure Perses + +Create a Global Datasource in Perses: + +```yaml +kind: GlobalDatasource +metadata: + name: influxdb-v1 +spec: + default: false + plugin: + kind: InfluxDBDatasource + spec: + version: v1 + directUrl: http://localhost:8086 + database: testdb +``` + +### 4. Create Queries + +Example InfluxQL queries: + +```sql +-- Get CPU usage for a specific host +SELECT * FROM cpu WHERE host='server01' AND time > now() - 1h + +-- Get average memory usage per host +SELECT mean(value) as avg_memory FROM memory WHERE time > now() - 1h GROUP BY host + +-- Get disk usage with region filtering +SELECT * FROM disk WHERE region='us-west' AND time > now() - 1h + +-- Get network metrics +SELECT bytes_in, bytes_out FROM network WHERE host='server02' AND time > now() - 1h +``` + +## Architecture + +### InfluxDB V1 +- **Host**: localhost +- **Port**: 8086 +- **Database**: testdb +- **Authentication**: None (default) +- **HTTP Endpoint**: `/query` + +### InfluxDB V3 +- **Host**: localhost +- **Port**: 8087 +- **Organization**: testorg +- **Bucket**: testbucket +- **Token**: test-token-12345 +- **HTTP Endpoint**: `/api/v2/query` + +## Data Generation Details + +The `generate-influxdb-testdata.py` script: + +1. **Uses HTTP API**: Sends data via POST to `/write?db=database` endpoint +2. **Line Protocol Format**: Uses InfluxDB line protocol for efficiency +3. **Batch Insertion**: Inserts 1000 lines per HTTP request +4. **Realistic Data**: + - CPU: 20-50% with occasional spikes + - Memory: 4-8 GB with garbage collection patterns + - Disk: 1-10 TB with slow growth + - Network: Variable bytes in/out with traffic spikes + - Temperature: 20-25°C with natural drift + +### Measurements + +**cpu** +- Tags: host (server01-04), region (us-west, us-east, eu-west), core (0-3) +- Fields: value (0-100%) + +**memory** +- Tags: host (server01-04), region +- Fields: value (1024-8192 MB) + +**disk** +- Tags: host, region, device (sda, sdb, sdc) +- Fields: value (0-10000 GB) + +**network** +- Tags: host, region, interface (eth0, eth1) +- Fields: bytes_in, bytes_out + +**temperature** +- Tags: host, room (rack01, rack02, rack03) +- Fields: celsius (15-35°C) + +## Useful Commands + +### View Available Measurements +```bash +curl 'http://localhost:8086/query?db=testdb&q=SHOW+MEASUREMENTS' +``` + +### Query Data via HTTP +```bash +curl 'http://localhost:8086/query?db=testdb&q=SELECT+*+FROM+cpu+WHERE+host=%27server01%27+LIMIT+10' +``` + +### Insert Custom Data +```bash +curl -X POST 'http://localhost:8086/write?db=testdb' \ + --data-binary 'cpu,host=myhost,region=us-west,core=0 value=45.5 1645000000000000000' +``` + +### View Container Logs +```bash +docker-compose -f docker-compose.influxdb-test.yml logs -f influxdb-v1 +``` + +### Stop Services +```bash +docker-compose -f docker-compose.influxdb-test.yml down +``` + +### Remove All Data and Start Fresh +```bash +docker-compose -f docker-compose.influxdb-test.yml down -v +./start-influxdb-testenv.sh +python3 generate-influxdb-testdata.py +``` + +## Requirements + +- Docker and Docker Compose +- Python 3.6+ +- curl (optional, for manual testing) + +## Troubleshooting + +### Script Fails to Connect + +If you get "Error connecting to InfluxDB", make sure: +1. InfluxDB is running: `docker-compose ps` +2. Port 8086 is accessible: `curl http://localhost:8086/ping` +3. Give the container time to start (up to 30 seconds) + +### No Data Generated + +Check that: +1. Database exists: `curl 'http://localhost:8086/query?db=testdb&q=SHOW+DATABASES'` +2. Server is responding to writes: `curl -X POST 'http://localhost:8086/write?db=testdb' --data 'test,host=test value=1'` + +### High Memory Usage + +If containers use too much memory: +1. Reduce the `minutes` parameter in the script (default: 120) +2. Reduce `batch_size` if insert fails +3. Delete old data and regenerate + +## Integration with Perses Development + +1. Start InfluxDB test environment: + ```bash + cd plugins/influxdb/test-data + ./start-influxdb-testenv.sh + ``` + +2. In another terminal, start Perses: + ```bash + npm run dev + ``` + +3. Create a Global Datasource pointing to `http://localhost:8086` + +4. Test queries and explorer functionality + +## Advanced Usage + +### Generate More Data + +Edit the script to change: +```python +minutes = 120 # Change this to generate more/less data +``` + +### Custom Metrics + +Modify the generation functions: +```python +def generate_custom_metrics(minutes=60): + """Generate your custom metrics""" + lines = [] + base_time = datetime.now() - timedelta(minutes=minutes) + + for minute in range(minutes): + timestamp = int((base_time + timedelta(minutes=minute)).timestamp() * 1e9) + # Your metric generation logic + line = f'your_measurement,tag1=value1 field1=value {timestamp}' + lines.append(line) + + return lines +``` + +Then add it to `main()`: +```python +all_lines.extend(generate_custom_metrics(minutes)) +``` + +## Notes + +- The HTTP API approach is more reliable than CLI commands +- Data is inserted in batches for better performance +- Each run generates new timestamps, so you can run the script multiple times to add more data +- The test data uses realistic patterns (CPU spikes, memory GC, disk growth) + +For more information on InfluxDB HTTP API: +- [InfluxDB V1 Write API](https://docs.influxdata.com/influxdb/v1.8/guides/write_data/) +- [InfluxDB Line Protocol](https://docs.influxdata.com/influxdb/v1.8/write_protocols/line_protocol/) + diff --git a/influxdb/test-data/docker-compose.influxdb-test.yml b/influxdb/test-data/docker-compose.influxdb-test.yml new file mode 100644 index 000000000..0b8c8d751 --- /dev/null +++ b/influxdb/test-data/docker-compose.influxdb-test.yml @@ -0,0 +1,54 @@ +version: '3.8' + +services: + # InfluxDB V1 + influxdb-v1: + image: influxdb:1.8.10 + container_name: influxdb-v1 + ports: + - "8086:8086" + environment: + INFLUXDB_DB: testdb + INFLUXDB_ADMIN_USER: admin + INFLUXDB_ADMIN_PASSWORD: password + volumes: + - influxdb-v1-data:/var/lib/influxdb + - ./influxdb-v1-init.sh:/docker-entrypoint-initdb.d/init.sh + networks: + - perses-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8086/ping"] + interval: 10s + timeout: 5s + retries: 5 + + # InfluxDB V3 (using Cloud image as V3 standalone is not in Docker Hub) + influxdb-v3: + image: influxdb:latest + container_name: influxdb-v3 + ports: + - "8087:8086" + environment: + INFLUXDB_TOKEN: test-token-12345 + INFLUXDB_BUCKET: testbucket + INFLUXDB_ORG: testorg + INFLUXDB_ADMIN_USER: admin + INFLUXDB_ADMIN_PASSWORD: password + volumes: + - influxdb-v3-data:/var/lib/influxdb2 + networks: + - perses-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8086/health"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + influxdb-v1-data: + influxdb-v3-data: + +networks: + perses-network: + driver: bridge + diff --git a/influxdb/test-data/generate-influxdb-testdata.py b/influxdb/test-data/generate-influxdb-testdata.py new file mode 100755 index 000000000..f918e4f62 --- /dev/null +++ b/influxdb/test-data/generate-influxdb-testdata.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +""" +Generate realistic time-series test data for InfluxDB V1 +This script creates sample metrics that are useful for testing the Perses InfluxDB plugin +Uses InfluxDB HTTP API instead of CLI +""" + +import time +import math +import random +from datetime import datetime, timedelta +import urllib.request +import urllib.error +import sys + +def insert_data(host, port, database, lines): + """Insert data into InfluxDB V1 using HTTP line protocol""" + url = f"http://{host}:{port}/write?db={database}" + + # Prepare line protocol data + data = '\n'.join(lines).encode('utf-8') + + try: + request = urllib.request.Request(url, data=data, method='POST') + request.add_header('Content-Type', 'text/plain') + + with urllib.request.urlopen(request) as response: + status = response.status + if status != 204: + print(f"Warning: Expected 204 status, got {status}", file=sys.stderr) + + except urllib.error.HTTPError as e: + print(f"Error inserting data: HTTP {e.code}", file=sys.stderr) + print(f"Response: {e.read().decode('utf-8')}", file=sys.stderr) + sys.exit(1) + except urllib.error.URLError as e: + print(f"Error connecting to InfluxDB: {e.reason}", file=sys.stderr) + sys.exit(1) + +def generate_cpu_metrics(minutes=60): + """Generate realistic CPU usage metrics""" + lines = [] + base_time = datetime.now() - timedelta(minutes=minutes) + + hosts = ["server01", "server02", "server03", "server04"] + regions = ["us-west", "us-east", "eu-west"] + + for minute in range(minutes): + timestamp = int((base_time + timedelta(minutes=minute)).timestamp() * 1e9) + + for host in hosts: + for core in range(4): + # Simulate CPU with natural variations + base_load = random.uniform(20, 50) + noise = random.gauss(0, 5) + peak = 30 if random.random() < 0.1 else 0 # 10% chance of peak + + cpu_value = base_load + noise + peak + cpu_value = max(0, min(100, cpu_value)) # Clamp between 0-100 + + region = regions[hash(host) % len(regions)] + line = f'cpu,host={host},region={region},core={core} value={cpu_value:.2f} {timestamp}' + lines.append(line) + + return lines + +def generate_memory_metrics(minutes=60): + """Generate realistic memory usage metrics""" + lines = [] + base_time = datetime.now() - timedelta(minutes=minutes) + + hosts = ["server01", "server02", "server03", "server04"] + regions = ["us-west", "us-east", "eu-west"] + + # Initialize baseline memory per host + memory_state = {host: random.uniform(4000, 6000) for host in hosts} + + for minute in range(minutes): + timestamp = int((base_time + timedelta(minutes=minute)).timestamp() * 1e9) + + for host in hosts: + # Memory tends to grow slowly then drop (garbage collection) + if random.random() < 0.1: # 10% chance of GC + memory_state[host] -= random.uniform(500, 2000) + else: + memory_state[host] += random.uniform(0, 200) + + memory_state[host] = max(1024, min(8192, memory_state[host])) + + region = regions[hash(host) % len(regions)] + line = f'memory,host={host},region={region} value={memory_state[host]:.0f} {timestamp}' + lines.append(line) + + return lines + +def generate_disk_metrics(minutes=60): + """Generate realistic disk usage metrics""" + lines = [] + base_time = datetime.now() - timedelta(minutes=minutes) + + hosts = ["server01", "server02", "server03", "server04"] + devices = ["sda", "sdb", "sdc"] + regions = ["us-west", "us-east", "eu-west"] + + disk_state = {} + for host in hosts: + for device in devices: + disk_state[f"{host}_{device}"] = random.uniform(1000, 5000) + + for minute in range(minutes): + timestamp = int((base_time + timedelta(minutes=minute)).timestamp() * 1e9) + + for host in hosts: + for device in devices: + key = f"{host}_{device}" + # Disk usage slowly increases + disk_state[key] += random.uniform(0, 100) + disk_state[key] = max(0, min(10000, disk_state[key])) + + region = regions[hash(host) % len(regions)] + line = f'disk,host={host},region={region},device={device} value={disk_state[key]:.0f} {timestamp}' + lines.append(line) + + return lines + +def generate_network_metrics(minutes=60): + """Generate realistic network metrics""" + lines = [] + base_time = datetime.now() - timedelta(minutes=minutes) + + hosts = ["server01", "server02", "server03", "server04"] + interfaces = ["eth0", "eth1"] + regions = ["us-west", "us-east", "eu-west"] + + for minute in range(minutes): + timestamp = int((base_time + timedelta(minutes=minute)).timestamp() * 1e9) + + for host in hosts: + for interface in interfaces: + # Network traffic patterns + base_in = random.uniform(100, 1000) + base_out = random.uniform(50, 500) + + # Occasional spikes + if random.random() < 0.05: + base_in *= random.uniform(2, 5) + base_out *= random.uniform(2, 5) + + region = regions[hash(host) % len(regions)] + line = f'network,host={host},region={region},interface={interface} bytes_in={base_in:.0f},bytes_out={base_out:.0f} {timestamp}' + lines.append(line) + + return lines + +def generate_temperature_metrics(minutes=60): + """Generate realistic temperature metrics""" + lines = [] + base_time = datetime.now() - timedelta(minutes=minutes) + + hosts = ["server01", "server02", "server03", "server04"] + rooms = ["rack01", "rack02", "rack03"] + + temp_state = {} + for host in hosts: + temp_state[host] = random.uniform(20, 25) + + for minute in range(minutes): + timestamp = int((base_time + timedelta(minutes=minute)).timestamp() * 1e9) + + for host in hosts: + # Temperature drifts slowly + temp_state[host] += random.gauss(0, 0.2) + temp_state[host] = max(15, min(35, temp_state[host])) + + room = rooms[hash(host) % len(rooms)] + line = f'temperature,host={host},room={room} celsius={temp_state[host]:.2f} {timestamp}' + lines.append(line) + + return lines + +def main(): + host = "localhost" + port = 8086 + database = "testdb" + minutes = 120 # Generate 2 hours of data + batch_size = 1000 # Insert 1000 lines per request + + print("Generating InfluxDB V1 test data...") + print(f"Host: {host}:{port}, Database: {database}") + print(f"Time window: Last {minutes} minutes") + print(f"Batch size: {batch_size} lines per request") + print() + + all_lines = [] + + print("Generating CPU metrics...", end=" ", flush=True) + cpu_lines = generate_cpu_metrics(minutes) + all_lines.extend(cpu_lines) + print(f"✓ ({len(cpu_lines)} points)") + + print("Generating memory metrics...", end=" ", flush=True) + memory_lines = generate_memory_metrics(minutes) + all_lines.extend(memory_lines) + print(f"✓ ({len(memory_lines)} points)") + + print("Generating disk metrics...", end=" ", flush=True) + disk_lines = generate_disk_metrics(minutes) + all_lines.extend(disk_lines) + print(f"✓ ({len(disk_lines)} points)") + + print("Generating network metrics...", end=" ", flush=True) + network_lines = generate_network_metrics(minutes) + all_lines.extend(network_lines) + print(f"✓ ({len(network_lines)} points)") + + print("Generating temperature metrics...", end=" ", flush=True) + temp_lines = generate_temperature_metrics(minutes) + all_lines.extend(temp_lines) + print(f"✓ ({len(temp_lines)} points)") + + print() + print(f"Total data points: {len(all_lines)}") + print() + + # Insert data in batches + print("Inserting data into InfluxDB...") + total_batches = (len(all_lines) + batch_size - 1) // batch_size + + for batch_num in range(total_batches): + start_idx = batch_num * batch_size + end_idx = min(start_idx + batch_size, len(all_lines)) + batch_lines = all_lines[start_idx:end_idx] + + print(f" Batch {batch_num + 1}/{total_batches} ({len(batch_lines)} lines)...", end=" ", flush=True) + try: + insert_data(host, port, database, batch_lines) + print("✓") + except SystemExit: + print("✗") + raise + + print() + print("Test data generation complete!") + print() + print("Available measurements:") + print(" - cpu (with tags: host, region, core)") + print(" - memory (with tags: host, region)") + print(" - disk (with tags: host, region, device)") + print(" - network (with tags: host, region, interface, fields: bytes_in, bytes_out)") + print(" - temperature (with tags: host, room)") + print() + print("Sample queries:") + print(" SELECT * FROM cpu WHERE host='server01' LIMIT 100") + print(" SELECT mean(value) FROM memory GROUP BY host") + print(" SELECT * FROM disk WHERE region='us-west'") + +if __name__ == "__main__": + main() + diff --git a/influxdb/test-data/influxdb-v1/influxdb-v1-init.sh b/influxdb/test-data/influxdb-v1/influxdb-v1-init.sh new file mode 100755 index 000000000..ab405ed65 --- /dev/null +++ b/influxdb/test-data/influxdb-v1/influxdb-v1-init.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -e + +echo "Initializing InfluxDB V1 with test data..." + +# Create database +influx -execute "CREATE DATABASE testdb" + +# Create retention policy +influx -execute "CREATE RETENTION POLICY \"30days\" ON \"testdb\" DURATION 30d REPLICATION 1 DEFAULT" + +# Create measurement with sample data +influx -execute " +INSERT INTO testdb cpu,host=server01,region=us-west value=0.64 1434067467000000000 +INSERT INTO testdb cpu,host=server01,region=us-west value=0.60 1434067468000000000 +INSERT INTO testdb cpu,host=server02,region=us-west value=0.50 1434067469000000000 +INSERT INTO testdb cpu,host=server03,region=us-east value=0.75 1434067470000000000 +" + +# Create memory measurement +influx -execute " +INSERT INTO testdb memory,host=server01,region=us-west value=1024 1434067467000000000 +INSERT INTO testdb memory,host=server01,region=us-west value=2048 1434067468000000000 +INSERT INTO testdb memory,host=server02,region=us-west value=1536 1434067469000000000 +INSERT INTO testdb memory,host=server03,region=us-east value=2560 1434067470000000000 +" + +# Create disk measurement +influx -execute " +INSERT INTO testdb disk,host=server01,region=us-west,device=sda value=100 1434067467000000000 +INSERT INTO testdb disk,host=server01,region=us-west,device=sdb value=200 1434067468000000000 +INSERT INTO testdb disk,host=server02,region=us-west,device=sda value=150 1434067469000000000 +INSERT INTO testdb disk,host=server03,region=us-east,device=sda value=250 1434067470000000000 +" + +# Create network measurement +influx -execute " +INSERT INTO testdb network,host=server01,region=us-west,interface=eth0 bytes_in=1000,bytes_out=2000 1434067467000000000 +INSERT INTO testdb network,host=server01,region=us-west,interface=eth1 bytes_in=1500,bytes_out=2500 1434067468000000000 +INSERT INTO testdb network,host=server02,region=us-west,interface=eth0 bytes_in=800,bytes_out=1600 1434067469000000000 +INSERT INTO testdb network,host=server03,region=us-east,interface=eth0 bytes_in=1200,bytes_out=2400 1434067470000000000 +" + +# Create temperature measurement (for time-series demo) +influx -execute " +INSERT INTO testdb temperature,host=server01,room=rack01 celsius=22.5 1434067467000000000 +INSERT INTO testdb temperature,host=server01,room=rack01 celsius=22.6 1434067468000000000 +INSERT INTO testdb temperature,host=server02,room=rack02 celsius=23.0 1434067469000000000 +INSERT INTO testdb temperature,host=server03,room=rack01 celsius=21.8 1434067470000000000 +INSERT INTO testdb temperature,host=server01,room=rack01 celsius=22.7 1434067471000000000 +INSERT INTO testdb temperature,host=server02,room=rack02 celsius=23.1 1434067472000000000 +" + +echo "InfluxDB V1 initialization complete!" + diff --git a/influxdb/test-data/influxdb-v3/influxdb-v3-init.sh b/influxdb/test-data/influxdb-v3/influxdb-v3-init.sh new file mode 100644 index 000000000..01c0d7746 --- /dev/null +++ b/influxdb/test-data/influxdb-v3/influxdb-v3-init.sh @@ -0,0 +1,84 @@ +#!/bin/bash +set -e + +echo "Initializing InfluxDB V3 with test data..." + +# Note: For InfluxDB V3, we'll use the newer API approach +# V3 uses different data structure but compatible query API + +# Export token for influx CLI +export INFLUX_TOKEN="test-token-12345" +export INFLUX_ORG="testorg" +export INFLUX_BUCKET="testbucket" +export INFLUX_URL="http://localhost:8087" + +# Wait for InfluxDB to be ready +echo "Waiting for InfluxDB V3 to be ready..." +until curl -s -H "Authorization: Token $INFLUX_TOKEN" "$INFLUX_URL/health" > /dev/null 2>&1; do + echo "InfluxDB V3 not ready yet, waiting..." + sleep 5 +done + +echo "InfluxDB V3 is ready!" + +# Create bucket if not exists (usually created during init) +# For now we use the default bucket created by environment variables + +# Insert test data using line protocol +# CPU metrics +curl -X POST "$INFLUX_URL/api/v2/write?org=$INFLUX_ORG&bucket=$INFLUX_BUCKET" \ + -H "Authorization: Token $INFLUX_TOKEN" \ + -H "Content-Type: text/plain; charset=utf-8" \ + --data-binary @- << 'EOF' +cpu,host=server01,region=us-west,core=0 value=45.5 1434067467000000000 +cpu,host=server01,region=us-west,core=1 value=42.1 1434067468000000000 +cpu,host=server02,region=us-west,core=0 value=38.9 1434067469000000000 +cpu,host=server03,region=us-east,core=0 value=55.2 1434067470000000000 +EOF + +# Memory metrics +curl -X POST "$INFLUX_URL/api/v2/write?org=$INFLUX_ORG&bucket=$INFLUX_BUCKET" \ + -H "Authorization: Token $INFLUX_TOKEN" \ + -H "Content-Type: text/plain; charset=utf-8" \ + --data-binary @- << 'EOF' +memory,host=server01,region=us-west value=5120 1434067467000000000 +memory,host=server01,region=us-west value=5242 1434067468000000000 +memory,host=server02,region=us-west value=4864 1434067469000000000 +memory,host=server03,region=us-east value=6144 1434067470000000000 +EOF + +# Disk metrics +curl -X POST "$INFLUX_URL/api/v2/write?org=$INFLUX_ORG&bucket=$INFLUX_BUCKET" \ + -H "Authorization: Token $INFLUX_TOKEN" \ + -H "Content-Type: text/plain; charset=utf-8" \ + --data-binary @- << 'EOF' +disk,host=server01,region=us-west,device=sda value=3500 1434067467000000000 +disk,host=server01,region=us-west,device=sdb value=2800 1434067468000000000 +disk,host=server02,region=us-west,device=sda value=4200 1434067469000000000 +disk,host=server03,region=us-east,device=sda value=5600 1434067470000000000 +EOF + +# Network metrics +curl -X POST "$INFLUX_URL/api/v2/write?org=$INFLUX_ORG&bucket=$INFLUX_BUCKET" \ + -H "Authorization: Token $INFLUX_TOKEN" \ + -H "Content-Type: text/plain; charset=utf-8" \ + --data-binary @- << 'EOF' +network,host=server01,region=us-west,interface=eth0 bytes_in=1250,bytes_out=2150 1434067467000000000 +network,host=server01,region=us-west,interface=eth1 bytes_in=1875,bytes_out=3125 1434067468000000000 +network,host=server02,region=us-west,interface=eth0 bytes_in=1000,bytes_out=2000 1434067469000000000 +network,host=server03,region=us-east,interface=eth0 bytes_in=1500,bytes_out=3000 1434067470000000000 +EOF + +# Temperature metrics +curl -X POST "$INFLUX_URL/api/v2/write?org=$INFLUX_ORG&bucket=$INFLUX_BUCKET" \ + -H "Authorization: Token $INFLUX_TOKEN" \ + -H "Content-Type: text/plain; charset=utf-8" \ + --data-binary @- << 'EOF' +temperature,host=server01,room=rack01 celsius=22.5 1434067467000000000 +temperature,host=server01,room=rack01 celsius=22.6 1434067468000000000 +temperature,host=server02,room=rack02 celsius=23.2 1434067469000000000 +temperature,host=server03,room=rack01 celsius=21.9 1434067470000000000 +EOF + +echo "InfluxDB V3 initialization complete!" + diff --git a/influxdb/test-data/start-influxdb-testenv.sh b/influxdb/test-data/start-influxdb-testenv.sh new file mode 100755 index 000000000..7052e11d3 --- /dev/null +++ b/influxdb/test-data/start-influxdb-testenv.sh @@ -0,0 +1,161 @@ +#!/bin/bash + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ InfluxDB Test Setup for Perses Plugin Testing ║${NC}" +echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}" +echo "" + +# Check if docker-compose is available +if ! command -v docker-compose &> /dev/null && ! command -v docker &> /dev/null; then + echo -e "${RED}❌ Error: docker or docker-compose is not installed${NC}" + exit 1 +fi + +# Determine docker-compose command +if docker compose version &> /dev/null; then + DOCKER_COMPOSE="docker compose" +else + DOCKER_COMPOSE="docker-compose" +fi + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +echo -e "${YELLOW}1. Starting InfluxDB V1 and V3 containers...${NC}" +$DOCKER_COMPOSE -f "$SCRIPT_DIR/docker-compose.influxdb-test.yml" up -d + +echo -e "${YELLOW}2. Waiting for services to be healthy...${NC}" +echo " - InfluxDB V1 (port 8086)" +echo " - InfluxDB V3 (port 8087)" +echo " - Grafana (port 3000)" +echo "" + +# Wait for InfluxDB V1 +echo -n " Checking InfluxDB V1... " +for i in {1..30}; do + if curl -s http://localhost:8086/ping > /dev/null 2>&1; then + echo -e "${GREEN}✓${NC}" + break + fi + echo -n "." + sleep 2 +done + +# Wait for InfluxDB V3 +echo -n " Checking InfluxDB V3... " +for i in {1..30}; do + if curl -s http://localhost:8087/health > /dev/null 2>&1; then + echo -e "${GREEN}✓${NC}" + break + fi + echo -n "." + sleep 2 +done + +echo "" +echo -e "${YELLOW}3. Initializing test databases...${NC}" + +# Initialize InfluxDB V1 +echo " Initializing InfluxDB V1..." +docker exec influxdb-v1 influx -execute "CREATE DATABASE testdb" 2>/dev/null || true +docker exec influxdb-v1 influx -database testdb -execute "SHOW MEASUREMENTS" > /dev/null + +echo -e " ${GREEN}✓${NC} InfluxDB V1 ready" + +# Initialize InfluxDB V3 +echo " Initializing InfluxDB V3..." +sleep 5 # Give V3 more time to start +echo -e " ${GREEN}✓${NC} InfluxDB V3 ready" + +echo "" +echo -e "${YELLOW}4. Generating test data...${NC}" + +# Check if Python is available for advanced data generation +if command -v python3 &> /dev/null; then + echo " Running Python data generator..." + python3 "$SCRIPT_DIR/generate-influxdb-testdata.py" || echo " Note: Python generator had issues, but basic data is loaded" +else + echo " Python3 not available, using basic test data" +fi + +echo "" +echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ ✓ Setup Complete! ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}" +echo "" + +echo -e "${BLUE}Available InfluxDB Instances:${NC}" +echo "" +echo -e " ${YELLOW}InfluxDB V1${NC}" +echo " URL: http://localhost:8086" +echo " Database: testdb" +echo " Admin: admin / password" +echo " Measurements:" +echo " - cpu (tags: host, region, core)" +echo " - memory (tags: host, region)" +echo " - disk (tags: host, region, device)" +echo " - network (tags: host, region, interface)" +echo " - temperature (tags: host, room)" +echo "" + +echo -e " ${YELLOW}InfluxDB V3${NC}" +echo " URL: http://localhost:8087" +echo " Org: testorg" +echo " Bucket: testbucket" +echo " Token: test-token-12345" +echo " Admin: admin / password" +echo "" + +echo -e " ${YELLOW}Grafana (Optional)${NC}" +echo " URL: http://localhost:3000" +echo " Admin: admin / admin" +echo "" + +echo -e "${BLUE}Testing with Perses:${NC}" +echo "" +echo "1. Start Perses development server" +echo "" +echo "2. Create a Global Datasource for InfluxDB V1:" +echo "" +echo " Kind: GlobalDatasource" +echo " Name: influxdb-v1" +echo " Plugin: InfluxDBDatasource" +echo " Spec:" +echo " directUrl: http://localhost:8086" +echo " database: testdb" +echo "" + +echo "3. Create a Query:" +echo "" +echo " Query: SELECT * FROM cpu WHERE time > now() - 1h" +echo " Or: SELECT * FROM memory WHERE time > now() - 1h" +echo "" + +echo -e "${BLUE}Useful Commands:${NC}" +echo "" +echo " View InfluxDB V1 data:" +echo " curl 'http://localhost:8086/query?db=testdb&q=SHOW MEASUREMENTS'" +echo "" +echo " List measurements:" +echo " influx -database testdb -execute 'SHOW MEASUREMENTS'" +echo "" +echo " Query data:" +echo " influx -database testdb -execute 'SELECT * FROM cpu LIMIT 10'" +echo "" +echo " Stop containers:" +echo " docker-compose -f $SCRIPT_DIR/docker-compose.influxdb-test.yml down" +echo "" +echo " View logs:" +echo " docker-compose -f $SCRIPT_DIR/docker-compose.influxdb-test.yml logs -f" +echo "" + +echo -e "${GREEN}Ready to test! Open Perses and configure datasource.${NC}" + From 6f8b864631f5db89577b7ed0d341712cec42f5a3 Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Tue, 24 Feb 2026 23:39:16 +0100 Subject: [PATCH 7/8] feat: Add the InfluxDB Explorer support Signed-off-by: Pascal Zimmermann --- influxdb/package-lock.json | 5 - influxdb/package.json | 18 +-- influxdb/rsbuild.config.ts | 7 + influxdb/src/__tests__/ExposeModules.test.ts | 46 ------ .../src/__tests__/InfluxDBDatasource.test.ts | 76 ---------- .../InfluxDBPluginIntegration.test.ts | 107 ------------- .../__tests__/ModuleFederationExports.test.ts | 87 ----------- .../src/__tests__/SchemaValidation.test.ts | 109 -------------- .../datasource/influxdb/InfluxDBDatasource.ts | 2 +- .../datasource/influxdb/InfluxDBEditor.tsx | 21 +-- influxdb/src/explore/InfluxDBExplorer.tsx | 141 ++++++++++++------ influxdb/src/index-federation.ts | 1 - influxdb/src/index.ts | 1 - .../InfluxDBTimeSeriesQuery.ts | 88 +++++++---- influxdb/tsconfig.json | 7 +- 15 files changed, 186 insertions(+), 530 deletions(-) delete mode 100644 influxdb/src/__tests__/ExposeModules.test.ts delete mode 100644 influxdb/src/__tests__/InfluxDBDatasource.test.ts delete mode 100644 influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts delete mode 100644 influxdb/src/__tests__/ModuleFederationExports.test.ts delete mode 100644 influxdb/src/__tests__/SchemaValidation.test.ts diff --git a/influxdb/package-lock.json b/influxdb/package-lock.json index e5766aeaa..7d8bdeb9a 100644 --- a/influxdb/package-lock.json +++ b/influxdb/package-lock.json @@ -24,8 +24,6 @@ "typescript": "^5.6.3" }, "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", "@hookform/resolvers": "^3.2.0", "@mui/material": "^5.0.0", "@perses-dev/components": "^0.53.0-rc.1", @@ -35,10 +33,7 @@ "@perses-dev/plugin-system": "^0.53.0-rc.1", "@tanstack/react-query": "^4.39.1", "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", "immer": "^10.1.1", - "lodash": "^4.17.21", "react": "^17.0.2 || ^18.0.0", "react-dom": "^17.0.2 || ^18.0.0", "react-hook-form": "^7.52.2", diff --git a/influxdb/package.json b/influxdb/package.json index 11a86e84d..4575f0cc7 100644 --- a/influxdb/package.json +++ b/influxdb/package.json @@ -23,11 +23,7 @@ "main": "lib/cjs/index.js", "module": "lib/index.js", "types": "lib/index.d.ts", - "dependencies": {}, "peerDependencies": { - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", - "@mui/material": "^5.0.0", "@hookform/resolvers": "^3.2.0", "@perses-dev/components": "^0.53.0-rc.2", "@perses-dev/core": "^0.53.0-beta.4", @@ -36,15 +32,11 @@ "@perses-dev/plugin-system": "^0.53.0-rc.2", "@tanstack/react-query": "^4.39.1", "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "echarts": "5.5.0", "immer": "^10.1.1", - "lodash": "^4.17.21", "react": "^17.0.2 || ^18.0.0", "react-dom": "^17.0.2 || ^18.0.0", "react-hook-form": "^7.52.2", - "react-router-dom": "^6.27.0", - "zod": "^3.22.4" + "react-router-dom": "^6.27.0" }, "devDependencies": { "@swc/cli": "^0.4.1-nightly.20240914", @@ -58,13 +50,15 @@ "@types/react": "^18.3.12", "concurrently": "^8.2.2", "cross-env": "^7.0.3", - "jest": "^29.7.0", + "jest": "^30.2.0", "jest-environment-jsdom": "^29.7.0", "typescript": "^5.6.3" }, "files": [ - "dist", - "README.md" + "lib/**/*", + "__mf/**/*", + "mf-manifest.json", + "mf-stats.json" ], "perses": { "schemasPath": "schemas", diff --git a/influxdb/rsbuild.config.ts b/influxdb/rsbuild.config.ts index 4d8d22641..01c144e11 100644 --- a/influxdb/rsbuild.config.ts +++ b/influxdb/rsbuild.config.ts @@ -15,9 +15,16 @@ export default createConfigForPlugin({ shared: { react: { singleton: true, requiredVersion: false }, 'react-dom': { singleton: true, requiredVersion: false }, + 'date-fns': { singleton: true }, '@perses-dev/core': { singleton: true, requiredVersion: false }, '@perses-dev/plugin-system': { singleton: true, requiredVersion: false }, '@perses-dev/components': { singleton: true, requiredVersion: false }, + '@perses-dev/explore': { singleton: true }, + '@perses-dev/dashboards': { singleton: true }, + '@hookform/resolvers': { singleton: true }, + '@tanstack/react-query': { singleton: true }, + 'react-hook-form': { singleton: true }, + 'react-router-dom': { singleton: true }, }, }, }); diff --git a/influxdb/src/__tests__/ExposeModules.test.ts b/influxdb/src/__tests__/ExposeModules.test.ts deleted file mode 100644 index 09eb43a0e..000000000 --- a/influxdb/src/__tests__/ExposeModules.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import * as InfluxDBDatasourceModule from '../datasource/influxdb/InfluxDBDatasource'; -import * as InfluxDBTimeSeriesQueryModule from '../queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; - -describe('Module Federation Expose Modules', () => { - describe('InfluxDBDatasource.ts', () => { - it('should export default InfluxDBDatasource', () => { - expect(InfluxDBDatasourceModule.default).toBeDefined(); - expect(InfluxDBDatasourceModule.default).toHaveProperty('createClient'); - }); - - it('should have all required datasource properties', () => { - const datasource = InfluxDBDatasourceModule.default; - expect(datasource).toHaveProperty('createClient'); - expect(datasource).toHaveProperty('createInitialOptions'); - expect(datasource).toHaveProperty('OptionsEditorComponent'); - }); - }); - - describe('influxdb-time-series-query.ts', () => { - it('should export default InfluxDBTimeSeriesQuery', () => { - expect(InfluxDBTimeSeriesQueryModule.default).toBeDefined(); - expect(InfluxDBTimeSeriesQueryModule.default).toHaveProperty('getTimeSeriesData'); - }); - - it('should have all required query properties', () => { - const query = InfluxDBTimeSeriesQueryModule.default; - expect(query).toHaveProperty('getTimeSeriesData'); - expect(query).toHaveProperty('createInitialOptions'); - expect(query).toHaveProperty('OptionsEditorComponent'); - }); - }); -}); - diff --git a/influxdb/src/__tests__/InfluxDBDatasource.test.ts b/influxdb/src/__tests__/InfluxDBDatasource.test.ts deleted file mode 100644 index aeba924a8..000000000 --- a/influxdb/src/__tests__/InfluxDBDatasource.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { InfluxDBV1Datasource } from '../datasource/influxdb-v1/InfluxDBV1Datasource'; - -describe('InfluxDBDatasource', () => { - it('should have createClient method', () => { - expect(InfluxDBV1Datasource).toHaveProperty('createClient'); - expect(typeof InfluxDBV1Datasource.createClient).toBe('function'); - }); - - it('should have OptionsEditorComponent', () => { - expect(InfluxDBV1Datasource).toHaveProperty('OptionsEditorComponent'); - }); - - it('should have createInitialOptions method', () => { - expect(InfluxDBV1Datasource).toHaveProperty('createInitialOptions'); - expect(typeof InfluxDBV1Datasource.createInitialOptions).toBe('function'); - }); - - it('should create initial options with database', () => { - const initialOptions = InfluxDBV1Datasource.createInitialOptions(); - expect(initialOptions).toEqual({ - database: '', - }); - }); - - it('should throw error when no URL is provided', () => { - const spec = { - directUrl: undefined, - }; - const options = { - proxyUrl: undefined, - }; - - expect(() => { - InfluxDBV1Datasource.createClient(spec, options); - }).toThrow('No URL specified for InfluxDB v1 client'); - }); - - it('should use directUrl when provided', () => { - const spec = { - directUrl: 'http://localhost:8086', - }; - const options = { - proxyUrl: 'http://proxy:8086', - }; - - const client = InfluxDBV1Datasource.createClient(spec, options); - expect(client.options.datasourceUrl).toBe('http://localhost:8086'); - }); - - it('should fall back to proxyUrl when directUrl is not provided', () => { - const spec = { - directUrl: undefined, - }; - const options = { - proxyUrl: 'http://proxy:8086', - }; - - const client = InfluxDBV1Datasource.createClient(spec, options); - expect(client.options.datasourceUrl).toBe('http://proxy:8086'); - }); - -}); - diff --git a/influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts b/influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts deleted file mode 100644 index 10c338653..000000000 --- a/influxdb/src/__tests__/InfluxDBPluginIntegration.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { getPluginModule } from '../getPluginModule'; -import packageJson from '../../package.json'; - -describe('InfluxDB Plugin Integration', () => { - describe('Plugin Module Resource', () => { - it('should return valid PluginModuleResource', () => { - const pluginModule = getPluginModule(); - expect(pluginModule.kind).toBe('PluginModule'); - }); - - it('should have correct metadata from package.json', () => { - const pluginModule = getPluginModule(); - expect(pluginModule.metadata.name).toBe(packageJson.name); - expect(pluginModule.metadata.version).toBe(packageJson.version); - }); - - it('should include all plugins from package.json perses config', () => { - const pluginModule = getPluginModule(); - const expectedPlugins = packageJson.perses.plugins; - expect(pluginModule.spec.plugins).toEqual(expectedPlugins); - }); - - it('should have correct plugin kinds', () => { - const pluginModule = getPluginModule(); - const pluginKinds = pluginModule.spec.plugins.map((p: any) => p.kind); - expect(pluginKinds).toContain('Datasource'); - expect(pluginKinds).toContain('TimeSeriesQuery'); - }); - - it('should have correct plugin names', () => { - const pluginModule = getPluginModule(); - const pluginNames = pluginModule.spec.plugins.map((p: any) => p.spec.name); - expect(pluginNames).toContain('InfluxDBDatasource'); - expect(pluginNames).toContain('InfluxDBTimeSeriesQuery'); - }); - }); - - describe('Plugin Metadata', () => { - it('should have display names for all plugins', () => { - const pluginModule = getPluginModule(); - pluginModule.spec.plugins.forEach((plugin: any) => { - expect(plugin.spec.display).toBeDefined(); - expect(plugin.spec.display.name).toBeDefined(); - expect(plugin.spec.display.name).toBeTruthy(); - }); - }); - - it('should have valid plugin specifications', () => { - const pluginModule = getPluginModule(); - pluginModule.spec.plugins.forEach((plugin: any) => { - expect(plugin.kind).toBeTruthy(); - expect(plugin.spec).toBeDefined(); - expect(plugin.spec.name).toBeDefined(); - }); - }); - }); - - describe('Datasource Plugin Configuration', () => { - it('should have InfluxDB Datasource configured', () => { - const pluginModule = getPluginModule(); - const datasourcePlugin = pluginModule.spec.plugins.find( - (p: any) => p.kind === 'Datasource' && p.spec.name === 'InfluxDBDatasource' - ); - expect(datasourcePlugin).toBeDefined(); - }); - - it('should have correct Datasource display name', () => { - const pluginModule = getPluginModule(); - const datasourcePlugin = pluginModule.spec.plugins.find( - (p: any) => p.kind === 'Datasource' && p.spec.name === 'InfluxDBDatasource' - ); - expect(datasourcePlugin.spec.display.name).toBe('InfluxDB Datasource'); - }); - }); - - describe('TimeSeriesQuery Plugin Configuration', () => { - it('should have TimeSeriesQuery plugin configured', () => { - const pluginModule = getPluginModule(); - const queryPlugin = pluginModule.spec.plugins.find( - (p: any) => p.kind === 'TimeSeriesQuery' && p.spec.name === 'InfluxDBTimeSeriesQuery' - ); - expect(queryPlugin).toBeDefined(); - }); - - it('should have correct TimeSeriesQuery display name', () => { - const pluginModule = getPluginModule(); - const queryPlugin = pluginModule.spec.plugins.find( - (p: any) => p.kind === 'TimeSeriesQuery' && p.spec.name === 'InfluxDBTimeSeriesQuery' - ); - expect(queryPlugin.spec.display.name).toBe('InfluxDB Time Series Query'); - }); - }); -}); - diff --git a/influxdb/src/__tests__/ModuleFederationExports.test.ts b/influxdb/src/__tests__/ModuleFederationExports.test.ts deleted file mode 100644 index aa7b957d6..000000000 --- a/influxdb/src/__tests__/ModuleFederationExports.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import * as InfluxDBDatasourceModule from '../datasource/influxdb-v1/InfluxDBV1Datasource'; -import * as InfluxDBTimeSeriesQueryModule from '../queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; - -describe('Module Federation Exports', () => { - describe('InfluxDBV1Datasource Module', () => { - it('should export default InfluxDBV1Datasource', () => { - expect(InfluxDBDatasourceModule.default).toBeDefined(); - expect(InfluxDBDatasourceModule.default).toHaveProperty('createClient'); - }); - - it('should export named InfluxDBV1Datasource', () => { - expect(InfluxDBDatasourceModule.InfluxDBV1Datasource).toBeDefined(); - expect(InfluxDBDatasourceModule.InfluxDBV1Datasource).toEqual(InfluxDBDatasourceModule.default); - }); - - it('should have all required datasource plugin properties', () => { - const datasource = InfluxDBDatasourceModule.default; - expect(datasource).toHaveProperty('createClient'); - expect(datasource).toHaveProperty('createInitialOptions'); - expect(datasource).toHaveProperty('OptionsEditorComponent'); - }); - }); - - describe('InfluxDBTimeSeriesQuery Module', () => { - it('should export default InfluxDBTimeSeriesQuery', () => { - expect(InfluxDBTimeSeriesQueryModule.default).toBeDefined(); - expect(InfluxDBTimeSeriesQueryModule.default).toHaveProperty('getTimeSeriesData'); - }); - - it('should export named InfluxDBTimeSeriesQuery', () => { - expect(InfluxDBTimeSeriesQueryModule.InfluxDBTimeSeriesQuery).toBeDefined(); - expect(InfluxDBTimeSeriesQueryModule.InfluxDBTimeSeriesQuery).toEqual(InfluxDBTimeSeriesQueryModule.default); - }); - - it('should have all required query plugin properties', () => { - const query = InfluxDBTimeSeriesQueryModule.default; - expect(query).toHaveProperty('getTimeSeriesData'); - expect(query).toHaveProperty('createInitialOptions'); - expect(query).toHaveProperty('OptionsEditorComponent'); - }); - }); - - describe('Bootstrap Module', () => { - it('should export all required modules from bootstrap', async () => { - const bootstrap = await import('../bootstrap'); - expect(bootstrap.getPluginModule).toBeDefined(); - expect(bootstrap.InfluxDBDatasource).toBeDefined(); - expect(bootstrap.InfluxDBTimeSeriesQuery).toBeDefined(); - }); - - it('should export correct InfluxDBDatasource from bootstrap', async () => { - const bootstrap = await import('../bootstrap'); - const datasource = bootstrap.InfluxDBDatasource; - expect(datasource).toHaveProperty('createClient'); - expect(datasource).toHaveProperty('createInitialOptions'); - expect(datasource).toHaveProperty('OptionsEditorComponent'); - }); - - it('should export correct InfluxDBTimeSeriesQuery from bootstrap', async () => { - const bootstrap = await import('../bootstrap'); - expect(bootstrap.InfluxDBTimeSeriesQuery).toEqual(InfluxDBTimeSeriesQueryModule.InfluxDBTimeSeriesQuery); - }); - - it('should return valid PluginModule metadata', async () => { - const bootstrap = await import('../bootstrap'); - const pluginModule = bootstrap.getPluginModule(); - expect(pluginModule.kind).toBe('PluginModule'); - expect(pluginModule.metadata).toHaveProperty('name'); - expect(pluginModule.metadata).toHaveProperty('version'); - expect(pluginModule.spec).toHaveProperty('plugins'); - }); - }); -}); - diff --git a/influxdb/src/__tests__/SchemaValidation.test.ts b/influxdb/src/__tests__/SchemaValidation.test.ts deleted file mode 100644 index 910214f83..000000000 --- a/influxdb/src/__tests__/SchemaValidation.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import * as fs from 'fs'; -import * as path from 'path'; - -describe('Plugin Schema Validation', () => { - describe('Datasource CUE Schema', () => { - it('should have correct CUE schema file', () => { - const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); - expect(fs.existsSync(schemaPath)).toBe(true); - }); - - it('should contain #kind definition', () => { - const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); - const content = fs.readFileSync(schemaPath, 'utf-8'); - expect(content).toContain('#kind: "InfluxDBDatasource"'); - }); - - it('should contain kind assignment', () => { - const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); - const content = fs.readFileSync(schemaPath, 'utf-8'); - expect(content).toContain('kind: #kind'); - }); - - it('should contain #selector definition', () => { - const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); - const content = fs.readFileSync(schemaPath, 'utf-8'); - expect(content).toContain('#selector'); - expect(content).toContain('common.#datasourceSelector'); - expect(content).toContain('_kind: #kind'); - }); - - it('should have valid unified v1 and v3 configuration', () => { - const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); - const content = fs.readFileSync(schemaPath, 'utf-8'); - // Version selector - expect(content).toContain('version:'); - // Connection type (proxy or direct) - expect(content).toContain('#directUrl'); - expect(content).toContain('#proxy'); - // Auth via secret reference - expect(content).toContain('auth?: string'); - // V1 specific fields - expect(content).toContain('database?: string'); - // V3 specific fields - expect(content).toContain('organization?: string'); - expect(content).toContain('bucket?: string'); - // Import common proxy patterns - expect(content).toContain('commonProxy'); - }); - }); - - describe('Plugin Registration', () => { - it('should have InfluxDBDatasource in package.json', () => { - const packageJsonPath = path.join(__dirname, '../../package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); - expect(packageJson.perses).toBeDefined(); - expect(packageJson.perses.plugins).toBeDefined(); - - const datasourcePlugin = packageJson.perses.plugins.find( - (p: any) => p.kind === 'Datasource' && p.spec.name === 'InfluxDBDatasource' - ); - expect(datasourcePlugin).toBeDefined(); - }); - - it('should have InfluxDBTimeSeriesQuery in package.json', () => { - const packageJsonPath = path.join(__dirname, '../../package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); - - const queryPlugin = packageJson.perses.plugins.find( - (p: any) => p.kind === 'TimeSeriesQuery' && p.spec.name === 'InfluxDBTimeSeriesQuery' - ); - expect(queryPlugin).toBeDefined(); - }); - }); - - describe('Plugin Module Files', () => { - it('InfluxDBDatasource.ts should have valid TypeScript', () => { - const datasourcePath = path.join(__dirname, '../datasources/influxdb/InfluxDBDatasource.ts'); - const content = fs.readFileSync(datasourcePath, 'utf-8'); - expect(content).toContain('export const InfluxDBDatasource'); - expect(content).toContain('version'); - expect(content).toContain('queryV1'); - expect(content).toContain('queryV3SQL'); - expect(content).toContain('createClient'); - }); - - it('InfluxDBTimeSeriesQuery.ts should have valid TypeScript', () => { - const queryPath = path.join(__dirname, '../queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts'); - const content = fs.readFileSync(queryPath, 'utf-8'); - expect(content).toContain('export const InfluxDBTimeSeriesQuery'); - expect(content).toContain('export default InfluxDBTimeSeriesQuery'); - expect(content).toContain('getTimeSeriesData'); - expect(content).toContain('OptionsEditorComponent'); - }); - }); -}); - diff --git a/influxdb/src/datasource/influxdb/InfluxDBDatasource.ts b/influxdb/src/datasource/influxdb/InfluxDBDatasource.ts index e0ff615f6..afd330c7d 100644 --- a/influxdb/src/datasource/influxdb/InfluxDBDatasource.ts +++ b/influxdb/src/datasource/influxdb/InfluxDBDatasource.ts @@ -40,7 +40,7 @@ function validateReadOnlyQuery(query: string): void { const firstWord = trimmedQuery.split(' ')[0]?.toUpperCase() || 'UNKNOWN'; throw new Error( `Write operations are not allowed. Query cannot start with: ${firstWord}. ` + - 'Only read-only queries (SELECT, SHOW, etc.) are permitted.' + 'Only read-only queries (SELECT, SHOW, etc.) are permitted.' ); } } diff --git a/influxdb/src/datasource/influxdb/InfluxDBEditor.tsx b/influxdb/src/datasource/influxdb/InfluxDBEditor.tsx index 0f286dc94..bd770675d 100644 --- a/influxdb/src/datasource/influxdb/InfluxDBEditor.tsx +++ b/influxdb/src/datasource/influxdb/InfluxDBEditor.tsx @@ -10,16 +10,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { - TextField, - Box, - Stack, - FormControl, - InputLabel, - Select, - MenuItem, - Typography, -} from '@mui/material'; +import { TextField, Box, Stack, FormControl, InputLabel, Select, MenuItem, Typography } from '@mui/material'; import { HTTPSettingsEditor } from '@perses-dev/plugin-system'; import { HTTPDatasourceSpec } from '@perses-dev/core'; import React, { ReactElement } from 'react'; @@ -78,8 +69,8 @@ export function InfluxDBEditor(props: InfluxDBEditorProps): ReactElement { const initialSpecDirect = version === 'v1' ? initialSpecDirectV1 : initialSpecDirectV3; const initialSpecProxy = version === 'v1' ? initialSpecProxyV1 : initialSpecProxyV3; // Handle changes from HTTP Settings - const handleHTTPSettingsChange = (next: any) => { - const updated = next as InfluxDBSpec; + const handleHTTPSettingsChange = (next: HTTPDatasourceSpec) => { + const updated = { ...value, ...next }; // If this is a proxy config, ensure it has the correct version-specific endpoints if (updated.proxy?.spec) { const correctEndpoints = version === 'v1' ? allowedEndpointsV1 : allowedEndpointsV3; @@ -92,9 +83,9 @@ export function InfluxDBEditor(props: InfluxDBEditorProps): ReactElement { allowedEndpoints: correctEndpoints, }, }, - }); + } as InfluxDBSpec); } else { - onChange(updated); + onChange(updated as InfluxDBSpec); } }; return ( @@ -137,7 +128,7 @@ export function InfluxDBEditor(props: InfluxDBEditorProps): ReactElement { {/* 2. HTTP Settings (Direct/Proxy) with version-specific endpoints */} + const definitions = queries.length + ? queries.map((query) => { + return { + kind: query.spec.plugin.kind, + spec: query.spec.plugin.spec, + }; + }) + : []; + return ( - + + + + + + ); +} + +function MetricDataTable({ queries }: { queries: QueryDefinition[] }): ReactElement { + const height = PANEL_PREVIEW_HEIGHT; + + // map TimeSeriesQueryDefinition to Definition + const definitions = queries.map((query) => { + return { + kind: query.spec.plugin.kind, + spec: query.spec.plugin.spec, + }; + }); + + return ( + + + + + ); } export function InfluxDBExplorer(): ReactElement { const { - data: { queries = [] }, + data: { tab = 'table', queries = [] }, setData, } = useExplorerManagerContext(); const [queryDefinitions, setQueryDefinitions] = useState(queries); - // map QueryDefinition to Definition - const definitions = queries.length - ? queries.map((query: QueryDefinition) => { - return { - kind: query.spec.plugin.kind, - spec: query.spec.plugin.spec, - }; - }) - : []; - return ( - setQueryDefinitions(state)} - queries={queryDefinitions} - onQueryRun={() => setData({ queries: queryDefinitions })} - /> - - - - - + setData({ tab: state, queries })} + variant="scrollable" + sx={{ borderBottom: 1, borderColor: 'divider' }} + > + + + + + {tab === 'table' && ( + + setQueryDefinitions(state)} + queries={queryDefinitions} + onQueryRun={() => setData({ tab, queries: queryDefinitions })} + filteredQueryPlugins={FILTERED_QUERY_PLUGINS} + /> + + + )} + {tab === 'graph' && ( + + setQueryDefinitions(state)} + queries={queryDefinitions} + onQueryRun={() => setData({ tab, queries: queryDefinitions })} + filteredQueryPlugins={FILTERED_QUERY_PLUGINS} + /> + + + )} + ); } diff --git a/influxdb/src/index-federation.ts b/influxdb/src/index-federation.ts index f139775e5..b93c7a026 100644 --- a/influxdb/src/index-federation.ts +++ b/influxdb/src/index-federation.ts @@ -1,2 +1 @@ import('./bootstrap'); - diff --git a/influxdb/src/index.ts b/influxdb/src/index.ts index e68903ca9..d30032c1d 100644 --- a/influxdb/src/index.ts +++ b/influxdb/src/index.ts @@ -15,4 +15,3 @@ export * from './explore'; export { getPluginModule } from './getPluginModule'; export * from './datasource/influxdb/InfluxDBDatasource'; export * from './queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; - diff --git a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts index 673587c7f..e79dd96ba 100644 --- a/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts +++ b/influxdb/src/queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts @@ -11,21 +11,37 @@ // See the License for the specific language governing permissions and // limitations under the License. import { TimeSeriesQueryPlugin, replaceVariables } from '@perses-dev/plugin-system'; -import { TimeSeriesData, TimeSeries, DatasourceSelector } from '@perses-dev/core'; +import { TimeSeriesData, TimeSeries, DatasourceSelector, AbsoluteTimeRange } from '@perses-dev/core'; import { InfluxDBTimeSeriesQueryEditor } from './InfluxDBTimeSeriesQueryEditor'; export interface InfluxDBTimeSeriesQuerySpec { datasource?: DatasourceSelector; query: string; } -function convertV1ResponseToTimeSeries(response: any): TimeSeries[] { +interface InfluxDBV1SeriesData { + name: string; + columns: string[]; + values: Array>; + tags?: Record; +} + +interface InfluxDBV1ResponseData { + results: Array<{ + series?: InfluxDBV1SeriesData[]; + }>; +} + +function convertV1ResponseToTimeSeries(response: InfluxDBV1ResponseData): TimeSeries[] { const series: TimeSeries[] = []; if (response.results && response.results[0] && response.results[0].series) { - response.results[0].series.forEach((seriesData: any) => { + response.results[0].series.forEach((seriesData: InfluxDBV1SeriesData) => { const timeIndex = seriesData.columns.indexOf('time'); const valueColumns = seriesData.columns.filter((col: string) => col !== 'time'); valueColumns.forEach((valueColumn: string) => { const valueIndex = seriesData.columns.indexOf(valueColumn); - const values = seriesData.values.map((row: any[]) => [new Date(row[timeIndex]).getTime(), row[valueIndex]]); + const values = seriesData.values.map((row: Array) => [ + new Date(row[timeIndex] as string | number).getTime(), + row[valueIndex], + ]); const tagStr = seriesData.tags ? Object.entries(seriesData.tags) .map(([k, v]) => k + '="' + v + '"') @@ -43,14 +59,34 @@ function convertV1ResponseToTimeSeries(response: any): TimeSeries[] { } return series; } -function convertV3ResponseToTimeSeries(response: any): TimeSeries[] { +function convertV3ResponseToTimeSeries(_response: Record): TimeSeries[] { const series: TimeSeries[] = []; // TODO: Implement V3 response conversion // V3 CSV/Flux response format return series; } +interface InfluxDBClient { + queryV1?: (query: string, database: string) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + queryV3SQL?: (query: string) => Promise>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + queryV3Flux?: (query: string) => Promise>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +interface QueryContext { + timeRange: AbsoluteTimeRange; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + variableState: Record; + datasourceStore: { + getDatasourceClient: (selector: DatasourceSelector) => Promise; + getDatasourceSpec?: (selector: DatasourceSelector) => Promise>; + }; +} + export const InfluxDBTimeSeriesQuery: TimeSeriesQueryPlugin = { - getTimeSeriesData: async (spec: InfluxDBTimeSeriesQuerySpec, context: any) => { + getTimeSeriesData: async (spec: InfluxDBTimeSeriesQuerySpec, context: QueryContext) => { // Return empty if query is empty if (!spec.query) { return { @@ -65,56 +101,56 @@ export const InfluxDBTimeSeriesQuery: TimeSeriesQueryPlugin; + let datasourceSpec: Record | undefined; + // Try to get datasource spec to determine a version try { datasourceSpec = await context.datasourceStore.getDatasourceSpec?.( spec.datasource ?? { kind: 'InfluxDBDatasource' } ); - } catch (e) { + } catch { // Spec not available, we'll try to detect from client methods } - // Determine version and call appropriate query method + // Determine version and call the appropriate query method if (typeof client.queryV1 === 'function') { // V1 Query const database = datasourceSpec?.database || ''; - response = await client.queryV1(query, database); + clientResponse = await client.queryV1(query, database as string); } else if (typeof client.queryV3SQL === 'function') { // V3 SQL Query - response = await client.queryV3SQL(query); + clientResponse = await client.queryV3SQL(query); } else if (typeof client.queryV3Flux === 'function') { // V3 Flux Query (fallback) - response = await client.queryV3Flux(query); + clientResponse = await client.queryV3Flux(query); } else { - throw new Error( - 'Datasource client has no query methods (queryV1, queryV3SQL, or queryV3Flux)' - ); + throw new Error('Datasource client has no query methods (queryV1, queryV3SQL, or queryV3Flux)'); } // Convert response to timeseries let timeSeries: TimeSeries[] = []; - if (response) { + if (clientResponse) { // Auto-detect V1 vs V3 based on response structure - if (response.results) { + if ('results' in clientResponse) { // V1 response format - timeSeries = convertV1ResponseToTimeSeries(response); - } else if (response.data) { + timeSeries = convertV1ResponseToTimeSeries(clientResponse as InfluxDBV1ResponseData); + } else if ('data' in clientResponse) { // V3 response format - timeSeries = convertV3ResponseToTimeSeries(response); + timeSeries = convertV3ResponseToTimeSeries(clientResponse); } } return { series: timeSeries, - timeRange: { start, end }, + timeRange: { start, end } as AbsoluteTimeRange, stepMs: 30 * 1000, metadata: { executedQueryString: query, diff --git a/influxdb/tsconfig.json b/influxdb/tsconfig.json index 3c43903cf..98221344c 100644 --- a/influxdb/tsconfig.json +++ b/influxdb/tsconfig.json @@ -1,3 +1,8 @@ { - "extends": "../tsconfig.json" + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist/lib", + "rootDir": "./src" + }, + "include": ["src"] } From f297a300da018976451b9fcef4d55d35a88fce4e Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Wed, 25 Feb 2026 10:04:41 +0100 Subject: [PATCH 8/8] feat: Add new unit tests Signed-off-by: Pascal Zimmermann --- influxdb/jest.config.ts | 27 +++- influxdb/src/setup-tests.ts | 17 +++ influxdb/src/test/ExposeModules.test.ts | 45 +++++++ influxdb/src/test/InfluxDBDatasource.test.ts | 79 ++++++++++++ .../test/InfluxDBPluginIntegration.test.ts | 116 +++++++++++++++++ .../src/test/ModuleFederationExports.test.ts | 86 +++++++++++++ influxdb/src/test/SchemaValidation.test.ts | 118 ++++++++++++++++++ 7 files changed, 482 insertions(+), 6 deletions(-) create mode 100644 influxdb/src/setup-tests.ts create mode 100644 influxdb/src/test/ExposeModules.test.ts create mode 100644 influxdb/src/test/InfluxDBDatasource.test.ts create mode 100644 influxdb/src/test/InfluxDBPluginIntegration.test.ts create mode 100644 influxdb/src/test/ModuleFederationExports.test.ts create mode 100644 influxdb/src/test/SchemaValidation.test.ts diff --git a/influxdb/jest.config.ts b/influxdb/jest.config.ts index 1c7f61ce7..393bcee65 100644 --- a/influxdb/jest.config.ts +++ b/influxdb/jest.config.ts @@ -1,7 +1,22 @@ -import type { Config } from 'jest'; -import sharedConfig from '../jest.shared'; -const config: Config = { - ...sharedConfig, - displayName: 'influxdb', +const path = require('path'); +const fs = require('fs'); + +// Read and parse the shared jest config inline to avoid module resolution issues +const swcrcPath = path.join(__dirname, '..', '.cjs.swcrc'); +const swcrc = JSON.parse(fs.readFileSync(swcrcPath, 'utf-8')); + +module.exports = { + testEnvironment: 'jsdom', + roots: ['/src'], + moduleNameMapper: { + '^echarts/(.*)$': 'echarts', + '^use-resize-observer$': 'use-resize-observer/polyfilled', + '\\.(css|less)$': '/../stylesMock.js', + }, + transformIgnorePatterns: ['node_modules/(?!(lodash-es|yaml))'], + transform: { + '^.+\\.(ts|tsx|js|jsx)$': ['@swc/jest', { ...swcrc, exclude: [], swcrc: false }], + }, + setupFilesAfterEnv: ['/src/setup-tests.ts'], }; -export default config; + diff --git a/influxdb/src/setup-tests.ts b/influxdb/src/setup-tests.ts new file mode 100644 index 000000000..ea1c9cf7c --- /dev/null +++ b/influxdb/src/setup-tests.ts @@ -0,0 +1,17 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '@testing-library/jest-dom'; + +// Always mock e-charts during tests since we don't have a proper canvas in jsdom +jest.mock('echarts'); diff --git a/influxdb/src/test/ExposeModules.test.ts b/influxdb/src/test/ExposeModules.test.ts new file mode 100644 index 000000000..b4201ac4a --- /dev/null +++ b/influxdb/src/test/ExposeModules.test.ts @@ -0,0 +1,45 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as InfluxDBDatasourceModule from '../datasource/influxdb/InfluxDBDatasource'; +import * as InfluxDBTimeSeriesQueryModule from '../queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; + +describe('Module Federation Expose Modules', () => { + describe('InfluxDBDatasource.ts', () => { + it('should export default InfluxDBDatasource', () => { + expect(InfluxDBDatasourceModule.default).toBeDefined(); + expect(InfluxDBDatasourceModule.default).toHaveProperty('createClient'); + }); + + it('should have all required datasource properties', () => { + const datasource = InfluxDBDatasourceModule.default; + expect(datasource).toHaveProperty('createClient'); + expect(datasource).toHaveProperty('createInitialOptions'); + expect(datasource).toHaveProperty('OptionsEditorComponent'); + }); + }); + + describe('influxdb-time-series-query.ts', () => { + it('should export default InfluxDBTimeSeriesQuery', () => { + expect(InfluxDBTimeSeriesQueryModule.default).toBeDefined(); + expect(InfluxDBTimeSeriesQueryModule.default).toHaveProperty('getTimeSeriesData'); + }); + + it('should have all required query properties', () => { + const query = InfluxDBTimeSeriesQueryModule.default; + expect(query).toHaveProperty('getTimeSeriesData'); + expect(query).toHaveProperty('createInitialOptions'); + expect(query).toHaveProperty('OptionsEditorComponent'); + }); + }); +}); diff --git a/influxdb/src/test/InfluxDBDatasource.test.ts b/influxdb/src/test/InfluxDBDatasource.test.ts new file mode 100644 index 000000000..061fdb09f --- /dev/null +++ b/influxdb/src/test/InfluxDBDatasource.test.ts @@ -0,0 +1,79 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { InfluxDBDatasource, InfluxDBSpec } from '../datasource/influxdb/InfluxDBDatasource'; + +describe('InfluxDBDatasource', () => { + it('should have createClient method', () => { + expect(InfluxDBDatasource).toHaveProperty('createClient'); + expect(typeof InfluxDBDatasource.createClient).toBe('function'); + }); + + it('should have OptionsEditorComponent', () => { + expect(InfluxDBDatasource).toHaveProperty('OptionsEditorComponent'); + }); + + it('should have createInitialOptions method', () => { + expect(InfluxDBDatasource).toHaveProperty('createInitialOptions'); + expect(typeof InfluxDBDatasource.createInitialOptions).toBe('function'); + }); + + it('should create initial options with version and database', () => { + const initialOptions = InfluxDBDatasource.createInitialOptions(); + expect(initialOptions).toEqual({ + version: 'v1', + database: '', + }); + }); + + it('should throw error when no URL is provided', () => { + const spec: InfluxDBSpec = { + version: 'v1', + database: 'testdb', + }; + const options = { + proxyUrl: undefined, + }; + + expect(() => { + InfluxDBDatasource.createClient(spec, options); + }).toThrow('No URL specified for InfluxDB client'); + }); + + it('should use directUrl when provided', () => { + const spec: InfluxDBSpec = { + version: 'v1', + database: 'testdb', + directUrl: 'http://localhost:8086', + }; + const options = { + proxyUrl: 'http://proxy:8086', + }; + + const client = InfluxDBDatasource.createClient(spec, options); + expect(client.options.datasourceUrl).toBe('http://localhost:8086'); + }); + + it('should fall back to proxyUrl when directUrl is not provided', () => { + const spec: InfluxDBSpec = { + version: 'v1', + database: 'testdb', + }; + const options = { + proxyUrl: 'http://proxy:8086', + }; + + const client = InfluxDBDatasource.createClient(spec, options); + expect(client.options.datasourceUrl).toBe('http://proxy:8086'); + }); +}); diff --git a/influxdb/src/test/InfluxDBPluginIntegration.test.ts b/influxdb/src/test/InfluxDBPluginIntegration.test.ts new file mode 100644 index 000000000..74d6543ed --- /dev/null +++ b/influxdb/src/test/InfluxDBPluginIntegration.test.ts @@ -0,0 +1,116 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { getPluginModule } from '../getPluginModule'; +import packageJson from '../../package.json'; + +interface PluginSpec { + kind: string; + spec: { + name: string; + display: { + name: string; + }; + }; +} + +describe('InfluxDB Plugin Integration', () => { + describe('Plugin Module Resource', () => { + it('should return valid PluginModuleResource', () => { + const pluginModule = getPluginModule(); + expect(pluginModule.kind).toBe('PluginModule'); + }); + + it('should have correct metadata from package.json', () => { + const pluginModule = getPluginModule(); + expect(pluginModule.metadata.name).toBe(packageJson.name); + expect(pluginModule.metadata.version).toBe(packageJson.version); + }); + + it('should include all plugins from package.json perses config', () => { + const pluginModule = getPluginModule(); + const expectedPlugins = packageJson.perses.plugins; + expect(pluginModule.spec.plugins).toEqual(expectedPlugins); + }); + + it('should have correct plugin kinds', () => { + const pluginModule = getPluginModule(); + const pluginKinds = pluginModule.spec.plugins.map((p: PluginSpec) => p.kind); + expect(pluginKinds).toContain('Datasource'); + expect(pluginKinds).toContain('TimeSeriesQuery'); + }); + + it('should have correct plugin names', () => { + const pluginModule = getPluginModule(); + const pluginNames = pluginModule.spec.plugins.map((p: PluginSpec) => p.spec.name); + expect(pluginNames).toContain('InfluxDBDatasource'); + expect(pluginNames).toContain('InfluxDBTimeSeriesQuery'); + }); + }); + + describe('Plugin Metadata', () => { + it('should have display names for all plugins', () => { + const pluginModule = getPluginModule(); + pluginModule.spec.plugins.forEach((plugin: PluginSpec) => { + expect(plugin.spec.display).toBeDefined(); + expect(plugin.spec.display.name).toBeDefined(); + expect(plugin.spec.display.name).toBeTruthy(); + }); + }); + + it('should have valid plugin specifications', () => { + const pluginModule = getPluginModule(); + pluginModule.spec.plugins.forEach((plugin: PluginSpec) => { + expect(plugin.kind).toBeTruthy(); + expect(plugin.spec).toBeDefined(); + expect(plugin.spec.name).toBeDefined(); + }); + }); + }); + + describe('Datasource Plugin Configuration', () => { + it('should have InfluxDB Datasource configured', () => { + const pluginModule = getPluginModule(); + const datasourcePlugin = pluginModule.spec.plugins.find( + (p: PluginSpec) => p.kind === 'Datasource' && p.spec.name === 'InfluxDBDatasource' + ); + expect(datasourcePlugin).toBeDefined(); + }); + + it('should have correct Datasource display name', () => { + const pluginModule = getPluginModule(); + const datasourcePlugin = pluginModule.spec.plugins.find( + (p: PluginSpec) => p.kind === 'Datasource' && p.spec.name === 'InfluxDBDatasource' + ); + expect(datasourcePlugin?.spec.display.name).toBe('InfluxDB Datasource'); + }); + }); + + describe('TimeSeriesQuery Plugin Configuration', () => { + it('should have TimeSeriesQuery plugin configured', () => { + const pluginModule = getPluginModule(); + const queryPlugin = pluginModule.spec.plugins.find( + (p: PluginSpec) => p.kind === 'TimeSeriesQuery' && p.spec.name === 'InfluxDBTimeSeriesQuery' + ); + expect(queryPlugin).toBeDefined(); + }); + + it('should have correct TimeSeriesQuery display name', () => { + const pluginModule = getPluginModule(); + const queryPlugin = pluginModule.spec.plugins.find( + (p: PluginSpec) => p.kind === 'TimeSeriesQuery' && p.spec.name === 'InfluxDBTimeSeriesQuery' + ); + expect(queryPlugin?.spec.display.name).toBe('InfluxDB Time Series Query'); + }); + }); +}); diff --git a/influxdb/src/test/ModuleFederationExports.test.ts b/influxdb/src/test/ModuleFederationExports.test.ts new file mode 100644 index 000000000..47c8f562a --- /dev/null +++ b/influxdb/src/test/ModuleFederationExports.test.ts @@ -0,0 +1,86 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as InfluxDBDatasourceModule from '../datasource/influxdb/InfluxDBDatasource'; +import * as InfluxDBTimeSeriesQueryModule from '../queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery'; + +describe('Module Federation Exports', () => { + describe('InfluxDBDatasource Module', () => { + it('should export default InfluxDBDatasource', () => { + expect(InfluxDBDatasourceModule.default).toBeDefined(); + expect(InfluxDBDatasourceModule.default).toHaveProperty('createClient'); + }); + + it('should export named InfluxDBDatasource', () => { + expect(InfluxDBDatasourceModule.InfluxDBDatasource).toBeDefined(); + expect(InfluxDBDatasourceModule.InfluxDBDatasource).toEqual(InfluxDBDatasourceModule.default); + }); + + it('should have all required datasource plugin properties', () => { + const datasource = InfluxDBDatasourceModule.default; + expect(datasource).toHaveProperty('createClient'); + expect(datasource).toHaveProperty('createInitialOptions'); + expect(datasource).toHaveProperty('OptionsEditorComponent'); + }); + }); + + describe('InfluxDBTimeSeriesQuery Module', () => { + it('should export default InfluxDBTimeSeriesQuery', () => { + expect(InfluxDBTimeSeriesQueryModule.default).toBeDefined(); + expect(InfluxDBTimeSeriesQueryModule.default).toHaveProperty('getTimeSeriesData'); + }); + + it('should export named InfluxDBTimeSeriesQuery', () => { + expect(InfluxDBTimeSeriesQueryModule.InfluxDBTimeSeriesQuery).toBeDefined(); + expect(InfluxDBTimeSeriesQueryModule.InfluxDBTimeSeriesQuery).toEqual(InfluxDBTimeSeriesQueryModule.default); + }); + + it('should have all required query plugin properties', () => { + const query = InfluxDBTimeSeriesQueryModule.default; + expect(query).toHaveProperty('getTimeSeriesData'); + expect(query).toHaveProperty('createInitialOptions'); + expect(query).toHaveProperty('OptionsEditorComponent'); + }); + }); + + describe('Bootstrap Module', () => { + it('should export all required modules from bootstrap', async () => { + const bootstrap = await import('../bootstrap'); + expect(bootstrap.getPluginModule).toBeDefined(); + expect(bootstrap.InfluxDBDatasource).toBeDefined(); + expect(bootstrap.InfluxDBTimeSeriesQuery).toBeDefined(); + }); + + it('should export correct InfluxDBDatasource from bootstrap', async () => { + const bootstrap = await import('../bootstrap'); + const datasource = bootstrap.InfluxDBDatasource; + expect(datasource).toHaveProperty('createClient'); + expect(datasource).toHaveProperty('createInitialOptions'); + expect(datasource).toHaveProperty('OptionsEditorComponent'); + }); + + it('should export correct InfluxDBTimeSeriesQuery from bootstrap', async () => { + const bootstrap = await import('../bootstrap'); + expect(bootstrap.InfluxDBTimeSeriesQuery).toEqual(InfluxDBTimeSeriesQueryModule.InfluxDBTimeSeriesQuery); + }); + + it('should return valid PluginModule metadata', async () => { + const bootstrap = await import('../bootstrap'); + const pluginModule = bootstrap.getPluginModule(); + expect(pluginModule.kind).toBe('PluginModule'); + expect(pluginModule.metadata).toHaveProperty('name'); + expect(pluginModule.metadata).toHaveProperty('version'); + expect(pluginModule.spec).toHaveProperty('plugins'); + }); + }); +}); diff --git a/influxdb/src/test/SchemaValidation.test.ts b/influxdb/src/test/SchemaValidation.test.ts new file mode 100644 index 000000000..453cd33df --- /dev/null +++ b/influxdb/src/test/SchemaValidation.test.ts @@ -0,0 +1,118 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as fs from 'fs'; +import * as path from 'path'; + +interface PluginSpec { + kind: string; + spec: { + name: string; + display: { + name: string; + }; + }; +} + +describe('Plugin Schema Validation', () => { + describe('Datasource CUE Schema', () => { + it('should have correct CUE schema file', () => { + const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); + expect(fs.existsSync(schemaPath)).toBe(true); + }); + + it('should contain #kind definition', () => { + const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); + const content = fs.readFileSync(schemaPath, 'utf-8'); + expect(content).toContain('#kind: "InfluxDBDatasource"'); + }); + + it('should contain kind assignment', () => { + const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); + const content = fs.readFileSync(schemaPath, 'utf-8'); + expect(content).toContain('kind: #kind'); + }); + + it('should contain #selector definition', () => { + const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); + const content = fs.readFileSync(schemaPath, 'utf-8'); + expect(content).toContain('#selector'); + expect(content).toContain('common.#datasourceSelector'); + expect(content).toContain('_kind: #kind'); + }); + + it('should have valid unified v1 and v3 configuration', () => { + const schemaPath = path.join(__dirname, '../../schemas/datasource/influxdb.cue'); + const content = fs.readFileSync(schemaPath, 'utf-8'); + // Version selector + expect(content).toContain('version:'); + // Connection type (proxy or direct) + expect(content).toContain('#directUrl'); + expect(content).toContain('#proxy'); + // Auth via secret reference + expect(content).toContain('auth?: string'); + // V1 specific fields + expect(content).toContain('database?: string'); + // V3 specific fields + expect(content).toContain('organization?: string'); + expect(content).toContain('bucket?: string'); + // Import common proxy patterns + expect(content).toContain('commonProxy'); + }); + }); + + describe('Plugin Registration', () => { + it('should have InfluxDBDatasource in package.json', () => { + const packageJsonPath = path.join(__dirname, '../../package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + expect(packageJson.perses).toBeDefined(); + expect(packageJson.perses.plugins).toBeDefined(); + + const datasourcePlugin = packageJson.perses.plugins.find( + (p: PluginSpec) => p.kind === 'Datasource' && p.spec.name === 'InfluxDBDatasource' + ); + expect(datasourcePlugin).toBeDefined(); + }); + + it('should have InfluxDBTimeSeriesQuery in package.json', () => { + const packageJsonPath = path.join(__dirname, '../../package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + + const queryPlugin = packageJson.perses.plugins.find( + (p: PluginSpec) => p.kind === 'TimeSeriesQuery' && p.spec.name === 'InfluxDBTimeSeriesQuery' + ); + expect(queryPlugin).toBeDefined(); + }); + }); + + describe('Plugin Module Files', () => { + it('InfluxDBDatasource.ts should have valid TypeScript', () => { + const datasourcePath = path.join(__dirname, '../datasource/influxdb/InfluxDBDatasource.ts'); + const content = fs.readFileSync(datasourcePath, 'utf-8'); + expect(content).toContain('export const InfluxDBDatasource'); + expect(content).toContain('version'); + expect(content).toContain('queryV1'); + expect(content).toContain('queryV3SQL'); + expect(content).toContain('createClient'); + }); + + it('InfluxDBTimeSeriesQuery.ts should have valid TypeScript', () => { + const queryPath = path.join(__dirname, '../queries/influxdb-time-series-query/InfluxDBTimeSeriesQuery.ts'); + const content = fs.readFileSync(queryPath, 'utf-8'); + expect(content).toContain('export const InfluxDBTimeSeriesQuery'); + expect(content).toContain('export default InfluxDBTimeSeriesQuery'); + expect(content).toContain('getTimeSeriesData'); + expect(content).toContain('OptionsEditorComponent'); + }); + }); +});