diff --git a/json/.cjs.swcrc b/json/.cjs.swcrc new file mode 100644 index 000000000..2ed65083d --- /dev/null +++ b/json/.cjs.swcrc @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true + }, + "target": "es2022", + "transform": { + "react": { + "runtime": "automatic", + "useBuiltins": true + } + } + }, + "module": { + "type": "commonjs" + }, + "exclude": ["\\.(stories|test)\\."] +} diff --git a/json/.eslintrc.js b/json/.eslintrc.js new file mode 100644 index 000000000..20cacfb0d --- /dev/null +++ b/json/.eslintrc.js @@ -0,0 +1,65 @@ +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + + plugins: ['import'], + + env: { + commonjs: true, + es6: true, + jest: true, + node: true, + browser: true, + }, + + parser: '@typescript-eslint/parser', + + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + + settings: { + react: { + version: 'detect', + }, + }, + + rules: { + 'prettier/prettier': 'error', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/array-type': [ + 'error', + { + default: 'array-simple', + }, + ], + 'import/order': 'error', + // you must disable the base rule as it can report incorrect errors + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + + 'react/prop-types': 'off', + 'react-hooks/exhaustive-deps': 'error', + // Not necessary in React 17 + 'react/react-in-jsx-scope': 'off', + 'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never', propElementValues: 'always' }], + + // We use this rule instead of the core eslint `no-duplicate-imports` + // because it avoids false errors on cases where we have a regular + // import and an `import type`. + 'import/no-duplicates': 'error', + }, + + ignorePatterns: ['**/dist'], +}; diff --git a/json/.gitignore b/json/.gitignore new file mode 100644 index 000000000..fe8baa8ba --- /dev/null +++ b/json/.gitignore @@ -0,0 +1,21 @@ +.idea/ + +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea + +# generated archives +*.tar.gz + +# external CUE dependencies +/*/cue.mod/pkg/ diff --git a/json/.swcrc b/json/.swcrc new file mode 100644 index 000000000..feaf67637 --- /dev/null +++ b/json/.swcrc @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true + }, + "target": "es2022", + "transform": { + "react": { + "runtime": "automatic", + "useBuiltins": true + } + } + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": ["\\.(stories|test)\\."] +} diff --git a/json/README.md b/json/README.md new file mode 100644 index 000000000..0a3444e13 --- /dev/null +++ b/json/README.md @@ -0,0 +1,43 @@ +# Plugin Module: json + +### How to install + +This plugin requires react and react-dom 17 or 18 + +Install peer dependencies: + +```bash +npm install react react-dom +``` + +Install the plugin: + +```bash +npm install @perses-dev/json +``` + +The Perses UI packages your plugin depends on are now maintained in the [`perses/shared`](https://github.com/perses/shared) repository. If you need to develop against local copies of those packages, follow the linking instructions in that repo. + +## Development + +### Setup + +Install dependencies: + +```bash +npm install +``` + +### Get Started + +Start the dev server: + +```bash +npm run dev +``` + +Build the plugin for distribution: + +```bash +npm run build +``` diff --git a/json/cue.mod/module.cue b/json/cue.mod/module.cue new file mode 100644 index 000000000..b8f16404f --- /dev/null +++ b/json/cue.mod/module.cue @@ -0,0 +1,17 @@ +module: "github.com/perses-dev/json@v0" +language: { + version: "v0.15.1" +} +source: { + kind: "git" +} +deps: { + "github.com/perses/shared/cue@v0": { + v: "v0.54.0" + default: true + } + "github.com/perses/spec/cue@v0": { + v: "v0.2.0" + default: true + } +} \ No newline at end of file diff --git a/json/go.mod b/json/go.mod new file mode 100644 index 000000000..652468ac4 --- /dev/null +++ b/json/go.mod @@ -0,0 +1,34 @@ +module github.com/perses-dev/json + +go 1.26.5 + +require ( + github.com/perses/perses v0.54.0 + github.com/perses/spec v0.2.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jpillora/backoff v1.0.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/perses/common v0.31.2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.70.0 // indirect + github.com/prometheus/procfs v0.21.0 // indirect + github.com/zitadel/oidc/v3 v3.48.1 // indirect + github.com/zitadel/schema v1.3.2 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/json/go.sum b/json/go.sum new file mode 100644 index 000000000..0312861f7 --- /dev/null +++ b/json/go.sum @@ -0,0 +1,64 @@ +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/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.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/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/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/common v0.31.2 h1:klsl0KfWn6wVVG4rDJvsTvFO8Owf5ed4nj2VjbQST60= +github.com/perses/common v0.31.2/go.mod h1:KgLB0ojBFzg93UwTNK8uAE1yuGexBiwqHiAvTFcHRDI= +github.com/perses/perses v0.54.0 h1:zfq0wkyjRPs1Em76PdTfWyzdPZKGyJDzo7QqxiBTkz0= +github.com/perses/perses v0.54.0/go.mod h1:Xq5Tv7gDdsx2sqph5Gbvx1GCym5un6GgjoOaDKhe9Qw= +github.com/perses/spec v0.2.0 h1:m/cdXVErAfjDft+dfxkfL+63Z+ggJ0sY08Wg7Q9Ruzk= +github.com/perses/spec v0.2.0/go.mod h1:fyW8gFeaTXbF2TaE0N+iWrHtC7UZiRcT13qo+Y0ZEJM= +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.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI= +github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY= +github.com/prometheus/procfs v0.21.0 h1:Qh/e6TlBjZf+XLLqNCqFGmCU6Kj/2Bu7kj3oAc0UnXc= +github.com/prometheus/procfs v0.21.0/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +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.48.1 h1:7uUWccuPbmwSLmmjFRFayWzqK7B8itjM8H8bBSTyr7Q= +github.com/zitadel/oidc/v3 v3.48.1/go.mod h1:HwoguOGo0eem0RK5Gb+P6Q4aQLVinJ9LhomlVEA57ck= +github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= +github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= +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.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/json/jest.config.ts b/json/jest.config.ts new file mode 100644 index 000000000..72799d2db --- /dev/null +++ b/json/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from "@jest/types"; +import shared from "../jest.shared"; + +const jestConfig: Config.InitialOptions = { + ...shared, + + setupFilesAfterEnv: [ + ...(shared.setupFilesAfterEnv ?? []), + "/src/setup-tests.ts", + ], +}; + +export default jestConfig; diff --git a/json/package.json b/json/package.json new file mode 100644 index 000000000..a81841a5c --- /dev/null +++ b/json/package.json @@ -0,0 +1,82 @@ +{ + "name": "@perses-dev/json", + "version": "0.1.0", + "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": { + "jsonata": "^2.0.5" + }, + "peerDependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@perses-dev/client": "^0.54.0", + "@perses-dev/components": "^0.54.0", + "@perses-dev/dashboards": "^0.54.0", + "@perses-dev/explore": "^0.54.0", + "@perses-dev/plugin-system": "^0.54.0", + "@perses-dev/spec": "file:../../spec", + "@tanstack/react-query": "^4.39.1", + "date-fns": "^4.1.0", + "lodash": "^4.17.21", + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0" + }, + "files": [ + "lib/**/*", + "__mf/**/*", + "mf-manifest.json", + "mf-stats.json" + ], + "perses": { + "metadata": { + "name": "Json", + "registry": "perses-dev" + }, + "schemasPath": "schemas", + "plugins": [ + { + "kind": "Datasource", + "spec": { + "display": { + "name": "JSON Datasource" + }, + "name": "JsonDatasource" + } + }, + { + "kind": "JsonQuery", + "spec": { + "display": { + "name": "JSON Query" + }, + "name": "JsonQuery" + } + } + ] + }, + "devDependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@hookform/resolvers": "^3.10.0", + "@tanstack/react-query": "^4.44.0", + "@tanstack/react-table": "^8.21.3", + "@uiw/react-codemirror": "^4.25.11", + "date-fns-tz": "^3.2.0", + "echarts": "^5.5.0", + "lodash": "^4.18.1", + "react-hook-form": "^7.85.0", + "use-resize-observer": "^9.1.0" + } +} diff --git a/json/rsbuild.config.ts b/json/rsbuild.config.ts new file mode 100644 index 000000000..182baedc4 --- /dev/null +++ b/json/rsbuild.config.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 { pluginReact } from '@rsbuild/plugin-react'; +import { createConfigForPlugin } from '../rsbuild.shared'; + +export default createConfigForPlugin({ + name: 'Json', + rsbuildConfig: { + plugins: [pluginReact()], + }, + moduleFederation: { + exposes: { + './JsonDatasource': './src/datasources/json-datasource', + './JsonQuery': './src/queries/json-query', + }, + shared: { + react: { requiredVersion: '18.2.0', singleton: true }, + 'react-dom': { requiredVersion: '18.2.0', singleton: true }, + 'date-fns': { singleton: true }, + 'date-fns-tz': { singleton: true }, + lodash: { singleton: true }, + '@emotion/react': { requiredVersion: '^11.11.3', singleton: true }, + '@emotion/styled': { singleton: true }, + '@hookform/resolvers': { singleton: true }, + '@perses-dev/components': { singleton: true }, + '@perses-dev/plugin-system': { singleton: true }, + '@perses-dev/explore': { singleton: true }, + '@perses-dev/dashboards': { singleton: true }, + '@perses-dev/client': { singleton: true }, + '@tanstack/react-query': { singleton: true }, + 'react-hook-form': { singleton: true }, + }, + }, +}); diff --git a/json/schemas/datasources/json-datasource/json-datasource.cue b/json/schemas/datasources/json-datasource/json-datasource.cue new file mode 100644 index 000000000..afbafec3d --- /dev/null +++ b/json/schemas/datasources/json-datasource/json-datasource.cue @@ -0,0 +1,15 @@ +package model + +import ( + "github.com/perses/shared/cue/common" + "github.com/perses/spec/cue/datasource" +) + +#kind: "JsonDatasource" + +kind: #kind +spec: { + datasource.#HTTPDatasourceSpec +} + +#selector: common.#datasourceSelector & {_kind: #kind} diff --git a/json/schemas/queries/json-query/query.cue b/json/schemas/queries/json-query/query.cue new file mode 100644 index 000000000..32134e22e --- /dev/null +++ b/json/schemas/queries/json-query/query.cue @@ -0,0 +1,19 @@ +package model + +import ( + "strings" +) + +kind: "JsonQuery" +spec: close({ + datasource?: { + kind: "JsonDatasource" + } + endpointUrl: strings.MinRunes(1) + method?: "GET" | "POST" + queryParams?: { + [string]: string + } + body?: string + jsonataExpression?: string +}) diff --git a/json/sdk/go/datasource/datasource.go b/json/sdk/go/datasource/datasource.go new file mode 100644 index 000000000..107b2cab9 --- /dev/null +++ b/json/sdk/go/datasource/datasource.go @@ -0,0 +1,63 @@ +// 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/go-sdk/datasource" + datasourceSpec "github.com/perses/spec/go/datasource" +) + +const PluginKind = "JsonDatasource" + +type Option func(plugin *Builder) error + +func create(options ...Option) (Builder, error) { + builder := &Builder{ + HTTPDatasourceSpec: datasourceSpec.HTTPDatasourceSpec{}, + } + + var defaults []Option + + for _, opt := range append(defaults, options...) { + if err := opt(builder); err != nil { + return *builder, err + } + } + + return *builder, nil +} + +type Builder struct { + datasourceSpec.HTTPDatasourceSpec `json:",inline" yaml:",inline"` +} + +func JsonDatasource(options ...Option) datasource.Option { + return func(builder *datasource.Builder) error { + plugin, err := create(options...) + if err != nil { + return err + } + + builder.Spec.Plugin.Kind = PluginKind + builder.Spec.Plugin.Spec = plugin.HTTPDatasourceSpec + return nil + } +} + +func Selector(datasourceName string) *datasource.Selector { + return &datasource.Selector{ + Kind: PluginKind, + Name: datasourceName, + } +} diff --git a/json/sdk/go/datasource/options.go b/json/sdk/go/datasource/options.go new file mode 100644 index 000000000..36d6c659d --- /dev/null +++ b/json/sdk/go/datasource/options.go @@ -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 datasource + +import "github.com/perses/perses/go-sdk/http" + +func DirectURL(url string) Option { + return func(builder *Builder) error { + builder.DirectURL = url + return nil + } +} + +func HTTPProxy(url string, options ...http.Option) Option { + return func(builder *Builder) error { + p, err := http.New(url, options...) + if err != nil { + return err + } + builder.Proxy = &p.Proxy + return nil + } +} diff --git a/json/sdk/go/query/options.go b/json/sdk/go/query/options.go new file mode 100644 index 000000000..80366aa2c --- /dev/null +++ b/json/sdk/go/query/options.go @@ -0,0 +1,61 @@ +// 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 query + +import ( + jsonDatasource "github.com/perses-dev/json/sdk/go/datasource" +) + +func Datasource(datasourceName string) Option { + return func(builder *Builder) error { + builder.Datasource = jsonDatasource.Selector(datasourceName) + return nil + } +} + +func EndpointURL(url string) Option { + return func(builder *Builder) error { + builder.EndpointURL = url + return nil + } +} + +func GET() Option { + return func(builder *Builder) error { + builder.Method = "GET" + return nil + } +} + +func POST(body string) Option { + return func(builder *Builder) error { + builder.Method = "POST" + builder.Body = body + return nil + } +} + +func QueryParams(params map[string]string) Option { + return func(builder *Builder) error { + builder.QueryParams = params + return nil + } +} + +func JSONataExpression(expr string) Option { + return func(builder *Builder) error { + builder.JSONataExpression = expr + return nil + } +} diff --git a/json/sdk/go/query/query.go b/json/sdk/go/query/query.go new file mode 100644 index 000000000..499878a66 --- /dev/null +++ b/json/sdk/go/query/query.go @@ -0,0 +1,63 @@ +// 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 query + +import ( + "github.com/perses/perses/go-sdk/datasource" + "github.com/perses/perses/go-sdk/query" + "github.com/perses/spec/go/plugin" +) + +const PluginKind = "JsonQuery" + +type PluginSpec struct { + Datasource *datasource.Selector `json:"datasource,omitempty" yaml:"datasource,omitempty"` + EndpointURL string `json:"endpointUrl" yaml:"endpointUrl"` + Method string `json:"method,omitempty" yaml:"method,omitempty"` + QueryParams map[string]string `json:"queryParams,omitempty" yaml:"queryParams,omitempty"` + Body string `json:"body,omitempty" yaml:"body,omitempty"` + JSONataExpression string `json:"jsonataExpression,omitempty" yaml:"jsonataExpression,omitempty"` +} + +type Option func(plugin *Builder) error + +func create(options ...Option) (Builder, error) { + builder := &Builder{ + PluginSpec: PluginSpec{}, + } + + for _, opt := range options { + if err := opt(builder); err != nil { + return *builder, err + } + } + + return *builder, nil +} + +type Builder struct { + PluginSpec `json:",inline" yaml:",inline"` +} + +func JsonQuery(options ...Option) query.Option { + plg, err := create(options...) + return query.Option{ + Kind: plugin.KindQuery, + Plugin: plugin.Plugin{ + Kind: PluginKind, + Spec: plg, + }, + Error: err, + } +} diff --git a/json/src/bootstrap.tsx b/json/src/bootstrap.tsx new file mode 100644 index 000000000..a51f61fa9 --- /dev/null +++ b/json/src/bootstrap.tsx @@ -0,0 +1,18 @@ +// 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 React from "react"; +import ReactDOM from "react-dom/client"; + +const root = ReactDOM.createRoot(document.getElementById('root')!); +root.render(); diff --git a/json/src/datasources/index.ts b/json/src/datasources/index.ts new file mode 100644 index 000000000..5c30117b7 --- /dev/null +++ b/json/src/datasources/index.ts @@ -0,0 +1 @@ +export * from './json-datasource'; diff --git a/json/src/datasources/json-datasource/JsonDatasource.tsx b/json/src/datasources/json-datasource/JsonDatasource.tsx new file mode 100644 index 000000000..745f8e2d5 --- /dev/null +++ b/json/src/datasources/json-datasource/JsonDatasource.tsx @@ -0,0 +1,60 @@ +// 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 } from "@perses-dev/client"; +import { DatasourcePlugin } from "@perses-dev/plugin-system"; +import { JsonDatasourceClient, JsonDatasourceSpec } from "./json-datasource-types"; +import { JsonDatasourceEditor } from "./JsonDatasourceEditor"; +import { buildUrl } from "./json-datasource-utils"; + +const createClient: DatasourcePlugin['createClient'] = (spec, options) => { + const { directUrl, proxy } = spec; + const { proxyUrl } = options; + + const datasourceUrl = directUrl ?? proxyUrl; + if (datasourceUrl === undefined) { + throw new Error('No URL specified for JsonDatasource client. You can use directUrl in the spec to configure it.'); + } + + const specHeaders = proxy?.spec.headers; + + return { + options: { + datasourceUrl, + }, + query: async (params, headers) => { + const { endpointUrl, method, queryParams, body } = params; + const url = buildUrl(datasourceUrl, endpointUrl, queryParams); + + const resolvedHeaders = headers ?? specHeaders; + const requestHeaders = + method === 'POST' && body + ? { 'Content-Type': 'application/json', ...resolvedHeaders } + : resolvedHeaders; + + const response = await fetch(url, { + method, + headers: requestHeaders, + body, + }); + + return await response.json(); + }, + }; +}; + +export const JsonDatasource: DatasourcePlugin = { + createClient, + OptionsEditorComponent: JsonDatasourceEditor, + createInitialOptions: () => ({ directUrl: '' }), +}; diff --git a/json/src/datasources/json-datasource/JsonDatasourceEditor.tsx b/json/src/datasources/json-datasource/JsonDatasourceEditor.tsx new file mode 100644 index 000000000..ad55f5aa7 --- /dev/null +++ b/json/src/datasources/json-datasource/JsonDatasourceEditor.tsx @@ -0,0 +1,48 @@ +// 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 { HTTPSettingsEditor } from "@perses-dev/plugin-system"; +import React, { ReactElement } from "react"; +import { JsonDatasourceSpec } from "./json-datasource-types"; + +export interface JsonDatasourceEditorProps { + value: JsonDatasourceSpec; + onChange: (next: JsonDatasourceSpec) => void; + isReadonly?: boolean; +} + +export function JsonDatasourceEditor({ value, onChange, isReadonly }: JsonDatasourceEditorProps): ReactElement { + const initialSpecDirect: JsonDatasourceSpec = { + directUrl: '', + }; + + const initialSpecProxy: JsonDatasourceSpec = { + proxy: { + kind: 'HTTPProxy', + spec: { + allowedEndpoints: [], + url: '', + }, + }, + }; + + return ( + + ); +} diff --git a/json/src/datasources/json-datasource/index.ts b/json/src/datasources/json-datasource/index.ts new file mode 100644 index 000000000..74e14eecc --- /dev/null +++ b/json/src/datasources/json-datasource/index.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. + +export { JsonDatasource } from './JsonDatasource'; +export type { JsonDatasourceSpec, JsonDatasourceClient, JsonDatasourceResponse } from './json-datasource-types'; + diff --git a/json/src/datasources/json-datasource/json-datasource-client.test.ts b/json/src/datasources/json-datasource/json-datasource-client.test.ts new file mode 100644 index 000000000..9b99052c7 --- /dev/null +++ b/json/src/datasources/json-datasource/json-datasource-client.test.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 { buildUrl } from './json-datasource-utils'; + +describe('buildUrl', () => { + it('joins base and path, stripping trailing slash from base', () => { + expect(buildUrl('http://api.example/', '/data')).toBe('http://api.example/data'); + }); + + it('prepends slash to path when missing', () => { + expect(buildUrl('http://api.example', 'data')).toBe('http://api.example/data'); + }); + + it('appends query params', () => { + expect(buildUrl('http://api.example', '/data', { foo: 'bar', baz: 'qux' })).toBe( + 'http://api.example/data?foo=bar&baz=qux' + ); + }); + + it('omits query string when params is empty', () => { + expect(buildUrl('http://api.example', '/data', {})).toBe('http://api.example/data'); + }); + + it('omits query string when params is undefined', () => { + expect(buildUrl('http://api.example', '/data')).toBe('http://api.example/data'); + }); + + it('handles path with no leading slash on base with trailing slash', () => { + expect(buildUrl('http://api.example/', 'data')).toBe('http://api.example/data'); + }); + + it('URL-encodes special characters in query param values', () => { + const url = buildUrl('http://api.example', '/search', { q: 'hello world' }); + expect(url).toBe('http://api.example/search?q=hello+world'); + }); +}); diff --git a/json/src/datasources/json-datasource/json-datasource-types.ts b/json/src/datasources/json-datasource/json-datasource-types.ts new file mode 100644 index 000000000..900da3a15 --- /dev/null +++ b/json/src/datasources/json-datasource/json-datasource-types.ts @@ -0,0 +1,39 @@ +// 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 { RequestHeaders } from "@perses-dev/client"; +import { DatasourceClient } from "@perses-dev/plugin-system"; +import { HTTPProxy } from "@perses-dev/spec"; + +export interface JsonDatasourceSpec { + directUrl?: string; + proxy?: HTTPProxy; +} + +export interface QueryRequestParameters { + endpointUrl: string; + method: string; + queryParams?: Record; + body?: string; +} + +interface JsonDatasourceClientOptions { + datasourceUrl: string; + headers?: RequestHeaders; +} + +export type JsonDatasourceResponse = unknown; +export interface JsonDatasourceClient extends DatasourceClient { + options: JsonDatasourceClientOptions; + query(params: QueryRequestParameters, headers?: RequestHeaders): Promise; +} diff --git a/json/src/datasources/json-datasource/json-datasource-utils.ts b/json/src/datasources/json-datasource/json-datasource-utils.ts new file mode 100644 index 000000000..4e70889fb --- /dev/null +++ b/json/src/datasources/json-datasource/json-datasource-utils.ts @@ -0,0 +1,23 @@ +// 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 function buildUrl(datasourceUrl: string, endpointUrl: string, queryParams?: Record): string { + const base = datasourceUrl.replace(/\/$/, ''); + const path = endpointUrl.startsWith('/') ? endpointUrl : `/${endpointUrl}`; + let url = `${base}${path}`; + if (queryParams && Object.keys(queryParams).length > 0) { + const searchParams = new URLSearchParams(queryParams); + url = `${url}?${searchParams.toString()}`; + } + return url; +} diff --git a/json/src/env.d.ts b/json/src/env.d.ts new file mode 100644 index 000000000..b0ac762b0 --- /dev/null +++ b/json/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/json/src/getPluginModule.ts b/json/src/getPluginModule.ts new file mode 100644 index 000000000..413f09bae --- /dev/null +++ b/json/src/getPluginModule.ts @@ -0,0 +1,17 @@ +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/json/src/index-federation.ts b/json/src/index-federation.ts new file mode 100644 index 000000000..b93c7a026 --- /dev/null +++ b/json/src/index-federation.ts @@ -0,0 +1 @@ +import('./bootstrap'); diff --git a/json/src/index.ts b/json/src/index.ts new file mode 100644 index 000000000..aee80e228 --- /dev/null +++ b/json/src/index.ts @@ -0,0 +1 @@ +export { getPluginModule } from './getPluginModule'; diff --git a/json/src/queries/index.ts b/json/src/queries/index.ts new file mode 100644 index 000000000..bf9ae23f8 --- /dev/null +++ b/json/src/queries/index.ts @@ -0,0 +1 @@ +export * from './json-query'; diff --git a/json/src/queries/json-query/JsonQuery.tsx b/json/src/queries/json-query/JsonQuery.tsx new file mode 100644 index 000000000..027cb72cd --- /dev/null +++ b/json/src/queries/json-query/JsonQuery.tsx @@ -0,0 +1,29 @@ +// 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 { JsonQueryPlugin, parseVariables } from "@perses-dev/plugin-system"; +import { getJsonData } from "./get-json-data"; +import { JsonQueryEditor } from "./JsonQueryEditor"; +import { JsonQuerySpec } from "./json-query-types"; + +export const JsonQuery: JsonQueryPlugin = { + getJsonData, + OptionsEditorComponent: JsonQueryEditor, + createInitialOptions: () => ({ endpointUrl: '', method: 'GET' }), + dependsOn: (spec) => { + const fromEndpoint = parseVariables(spec.endpointUrl); + const fromParams = Object.values(spec.queryParams ?? {}).flatMap(parseVariables); + const fromBody = spec.body ? parseVariables(spec.body) : []; + return { variables: [...new Set([...fromEndpoint, ...fromParams, ...fromBody])] }; + }, +}; diff --git a/json/src/queries/json-query/JsonQueryEditor.tsx b/json/src/queries/json-query/JsonQueryEditor.tsx new file mode 100644 index 000000000..edbb3216e --- /dev/null +++ b/json/src/queries/json-query/JsonQueryEditor.tsx @@ -0,0 +1,215 @@ +// 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 { + DatasourceSelect, + DatasourceSelectProps, + OptionsEditorProps +} from "@perses-dev/plugin-system"; +import { + Button, + FormControl, + IconButton, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, + Stack, + TextField, + Typography +} from "@mui/material"; +import { produce } from "immer"; +import { ReactElement } from "react"; +import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form"; +import { HttpMethod, JsonQuerySpec } from "./json-query-types"; +import { DATASOURCE_KIND, DEFAULT_DATASOURCE } from "./constants"; +import { isDefaultJsonDatasourceSelector, isJsonDatasourceSelector } from "./json-query-selectors"; +import CloseIcon from "mdi-material-ui/Close"; +import AddIcon from "mdi-material-ui/Plus"; + +type JsonQueryEditorProps = OptionsEditorProps; + +const HTTP_METHODS: HttpMethod[] = ['GET', 'POST']; + +interface QueryParamEntry { + key: string; + value: string; +} + +interface JsonQueryFormValues { + endpointUrl: string; + method?: HttpMethod; + queryParams: QueryParamEntry[]; + body?: string; + jsonataExpression?: string; +} + +function specToForm(spec: JsonQuerySpec): JsonQueryFormValues { + return { + endpointUrl: spec.endpointUrl, + method: spec.method, + queryParams: Object.entries(spec.queryParams ?? {}).map(([key, value]) => ({ key, value })), + body: spec.body, + jsonataExpression: spec.jsonataExpression, + }; +} + +function formToSpec(form: JsonQueryFormValues, original: JsonQuerySpec): JsonQuerySpec { + const queryParams = Object.fromEntries(form.queryParams.map(({ key, value }) => [key, value])); + return { + ...original, + endpointUrl: form.endpointUrl, + method: form.method, + queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined, + body: form.body || undefined, + jsonataExpression: form.jsonataExpression || undefined, + }; +} + +export function JsonQueryEditor(props: JsonQueryEditorProps): ReactElement { + const { onChange, value } = props; + const selectedDatasource = value.datasource ?? DEFAULT_DATASOURCE; + + const { register, handleSubmit, control } = useForm({ + mode: 'onBlur', + defaultValues: specToForm(value), + }); + + const { fields, append, remove } = useFieldArray({ control, name: 'queryParams' }); + const method = useWatch({ control, name: 'method' }); + + const commit = handleSubmit((formValues) => { + onChange(formToSpec(formValues, value)); + }); + + const handleDatasourceChange: DatasourceSelectProps['onChange'] = (next) => { + if (isJsonDatasourceSelector(next)) { + onChange(produce(value, (draft) => { + draft.datasource = isDefaultJsonDatasourceSelector(next) ? undefined : next; + })); + return; + } + throw new Error('Got unexpected non-JsonDatasource datasource selector'); + }; + + const handleMethodChange = (e: SelectChangeEvent): void => { + onChange(produce(value, (draft) => { + const method = e.target.value as HttpMethod; + draft.method = method; + if(method === 'GET') { + draft.body = undefined; + } + })); + }; + + return ( + + + + + + Method + ( + + {...field} + labelId="json-method-label" + label="Method" + value={field.value ?? 'GET'} + onChange={(e) => { + field.onChange(e); + handleMethodChange(e); + }} + > + {HTTP_METHODS.map((m) => ( + + {m} + + ))} + + )} + /> + + + + + + + + Query Parameters + + + {fields.map((field, index) => ( + + + + { remove(index); }} aria-label="Remove parameter"> + + + + ))} + + + {(method ?? 'GET') === 'POST' && ( + + )} + + Transformation + + + ); +} diff --git a/json/src/queries/json-query/constants.ts b/json/src/queries/json-query/constants.ts new file mode 100644 index 000000000..634a622d8 --- /dev/null +++ b/json/src/queries/json-query/constants.ts @@ -0,0 +1,2 @@ +export const DATASOURCE_KIND = 'JsonDatasource'; +export const DEFAULT_DATASOURCE = { kind: DATASOURCE_KIND }; diff --git a/json/src/queries/json-query/get-json-data.test.ts b/json/src/queries/json-query/get-json-data.test.ts new file mode 100644 index 000000000..be037a9cf --- /dev/null +++ b/json/src/queries/json-query/get-json-data.test.ts @@ -0,0 +1,190 @@ +// 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. + +const mockReplaceVariables: jest.Mock = jest.fn((text: string) => text); +jest.mock('@perses-dev/plugin-system', () => ({ + replaceVariables: (...args: unknown[]) => mockReplaceVariables(...args), +})); + +import { JsonDatasourceClient } from '../../datasources/json-datasource/json-datasource-types'; +import { JsonQuerySpec } from './json-query-types'; +import { getJsonData } from './get-json-data'; + +const makeClient = (): JsonDatasourceClient => ({ + options: { datasourceUrl: 'http://api.example' }, + query: jest.fn(async () => ({ items: ['a', 'b', 'c'] })), +}); + +function createContext(client: JsonDatasourceClient) { + return { + variableState: {}, + datasourceStore: { + getDatasource: jest.fn(), + getDatasourceClient: jest.fn(() => Promise.resolve(client)), + listDatasourceSelectItems: jest.fn(async () => []), + getLocalDatasources: jest.fn(), + setLocalDatasources: jest.fn(), + getSavedDatasources: jest.fn(), + setSavedDatasources: jest.fn(), + }, + }; +} + +describe('getJsonData', () => { + beforeEach(() => { + mockReplaceVariables.mockImplementation((text: string) => text); + }); + + it('calls client.query with the correct parameters', async () => { + const client = makeClient(); + const spec: JsonQuerySpec = { endpointUrl: '/data', method: 'GET' }; + + await getJsonData(spec, createContext(client) as unknown as Parameters[1]); + + expect(client.query).toHaveBeenCalledWith({ + endpointUrl: '/data', + method: 'GET', + queryParams: undefined, + body: undefined, + }); + }); + + it('returns the response from client.query directly when no JSONata expression', async () => { + const client = makeClient(); + const spec: JsonQuerySpec = { endpointUrl: '/data', method: 'GET' }; + + const result = await getJsonData(spec, createContext(client) as unknown as Parameters[1]); + + expect(result).toEqual({ data: { items: ['a', 'b', 'c'] } }); + }); + + it('defaults to GET when method is not specified', async () => { + const client = makeClient(); + const spec: JsonQuerySpec = { endpointUrl: '/data' }; + + await getJsonData(spec, createContext(client) as unknown as Parameters[1]); + + expect(client.query).toHaveBeenCalledWith(expect.objectContaining({ method: 'GET' })); + }); + + it('forwards query params to client.query', async () => { + const client = makeClient(); + const spec: JsonQuerySpec = { endpointUrl: '/search', method: 'GET', queryParams: { q: 'test', limit: '5' } }; + + await getJsonData(spec, createContext(client) as unknown as Parameters[1]); + + expect(client.query).toHaveBeenCalledWith( + expect.objectContaining({ queryParams: { q: 'test', limit: '5' } }) + ); + }); + + it('forwards body to client.query for POST', async () => { + const client = makeClient(); + const body = '{"filter":"active"}'; + const spec: JsonQuerySpec = { endpointUrl: '/query', method: 'POST', body }; + + await getJsonData(spec, createContext(client) as unknown as Parameters[1]); + + expect(client.query).toHaveBeenCalledWith(expect.objectContaining({ method: 'POST', body })); + }); + + it('passes variableState to replaceVariables for endpointUrl interpolation', async () => { + const client = makeClient(); + const variableState = { env: { value: 'prod', loading: false } }; + mockReplaceVariables.mockImplementation((text: string) => + text === '/api/$env/data' ? '/api/prod/data' : text + ); + const spec: JsonQuerySpec = { endpointUrl: '/api/$env/data', method: 'GET' }; + const context = { ...createContext(client), variableState }; + + await getJsonData(spec, context as unknown as Parameters[1]); + + expect(mockReplaceVariables).toHaveBeenCalledWith('/api/$env/data', variableState); + expect(client.query).toHaveBeenCalledWith(expect.objectContaining({ endpointUrl: '/api/prod/data' })); + }); + + it('passes variableState to replaceVariables for each query param value', async () => { + const client = makeClient(); + const variableState = { team: { value: 'ops', loading: false } }; + mockReplaceVariables.mockImplementation((text: string) => + text === '$team' ? 'ops' : text + ); + const spec: JsonQuerySpec = { endpointUrl: '/data', method: 'GET', queryParams: { team: '$team', env: 'prod' } }; + const context = { ...createContext(client), variableState }; + + await getJsonData(spec, context as unknown as Parameters[1]); + + expect(mockReplaceVariables).toHaveBeenCalledWith('$team', variableState); + expect(client.query).toHaveBeenCalledWith( + expect.objectContaining({ queryParams: { team: 'ops', env: 'prod' } }) + ); + }); + + it('passes variableState to replaceVariables for body interpolation', async () => { + const client = makeClient(); + const variableState = { env: { value: 'staging', loading: false } }; + mockReplaceVariables.mockImplementation((text: string) => + text === '{"env":"$env"}' ? '{"env":"staging"}' : text + ); + const spec: JsonQuerySpec = { endpointUrl: '/query', method: 'POST', body: '{"env":"$env"}' }; + const context = { ...createContext(client), variableState }; + + await getJsonData(spec, context as unknown as Parameters[1]); + + expect(mockReplaceVariables).toHaveBeenCalledWith('{"env":"$env"}', variableState); + expect(client.query).toHaveBeenCalledWith(expect.objectContaining({ body: '{"env":"staging"}' })); + }); + + describe('JSONata transformation', () => { + it('applies JSONata expression to the response', async () => { + const client = makeClient(); + client.query = jest.fn(async () => ({ users: [{ name: 'Alice' }, { name: 'Bob' }] })); + const spec: JsonQuerySpec = { endpointUrl: '/users', method: 'GET', jsonataExpression: 'users.name' }; + + const result = await getJsonData(spec, createContext(client) as unknown as Parameters[1]); + + expect(JSON.stringify(result)).toBe(JSON.stringify({ data: ['Alice', 'Bob'] })); + }); + + it('throws when JSONata expression matches nothing', async () => { + const client = makeClient(); + client.query = jest.fn(async () => ({ data: [] })); + const spec: JsonQuerySpec = { endpointUrl: '/data', method: 'GET', jsonataExpression: 'nonexistent.path' }; + + await expect( + getJsonData(spec, createContext(client) as unknown as Parameters[1]) + ).rejects.toThrow('JSONata expression did not match any data'); + }); + + it('returns raw response when jsonataExpression is not set', async () => { + const client = makeClient(); + const raw = { value: 99 }; + client.query = jest.fn(async () => raw); + const spec: JsonQuerySpec = { endpointUrl: '/data', method: 'GET' }; + + const result = await getJsonData(spec, createContext(client) as unknown as Parameters[1]); + + expect(result).toEqual({ data: raw }); + }); + }); + + it('returns empty array when API returns empty array', async () => { + const client = makeClient(); + client.query = jest.fn(async () => []); + const spec: JsonQuerySpec = { endpointUrl: '/empty', method: 'GET' }; + + const result = await getJsonData(spec, createContext(client) as unknown as Parameters[1]); + + expect(result).toEqual({ data: [] }); + }); +}); diff --git a/json/src/queries/json-query/get-json-data.ts b/json/src/queries/json-query/get-json-data.ts new file mode 100644 index 000000000..0070120b1 --- /dev/null +++ b/json/src/queries/json-query/get-json-data.ts @@ -0,0 +1,54 @@ +// 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 jsonata from "jsonata"; +import { JsonQueryPlugin, replaceVariables } from "@perses-dev/plugin-system"; +import { JsonDatasourceClient } from "../../datasources/json-datasource/json-datasource-types"; +import { JsonQuerySpec } from "./json-query-types"; +import { DEFAULT_DATASOURCE } from "./constants"; + +async function applyJsonata(data: unknown, expression: string): Promise { + const expr = jsonata(expression); + return expr.evaluate(data); +} + +export const getJsonData: JsonQueryPlugin['getJsonData'] = async (spec, context) => { + const endpointUrl = replaceVariables(spec.endpointUrl, context.variableState); + + const queryParams = spec.queryParams + ? Object.fromEntries( + Object.entries(spec.queryParams).map(([k, v]) => [k, replaceVariables(v, context.variableState)]) + ) + : undefined; + + const client = await context.datasourceStore.getDatasourceClient( + spec.datasource ?? DEFAULT_DATASOURCE + ); + + let response = await client.query({ + endpointUrl, + method: spec.method ?? 'GET', + queryParams, + body: spec.body ? replaceVariables(spec.body, context.variableState) : undefined, + }); + + + if (spec.jsonataExpression) { + response = await applyJsonata(response, spec.jsonataExpression); + if (response === undefined) { + throw new Error(`JSONata expression did not match any data. Please check the expression and the input data.`); + } + } + + return { data: response }; +}; diff --git a/json/src/queries/json-query/index.ts b/json/src/queries/json-query/index.ts new file mode 100644 index 000000000..8d6c9399d --- /dev/null +++ b/json/src/queries/json-query/index.ts @@ -0,0 +1,18 @@ +// 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 { JsonQuery } from './JsonQuery'; +export type { JsonQuerySpec, HttpMethod } from './json-query-types'; +export { isDefaultJsonDatasourceSelector, isJsonDatasourceSelector } from './json-query-selectors'; +export type { JsonDatasourceSelector } from './json-query-selectors'; + diff --git a/json/src/queries/json-query/json-query-selectors.ts b/json/src/queries/json-query/json-query-selectors.ts new file mode 100644 index 000000000..71b61581a --- /dev/null +++ b/json/src/queries/json-query/json-query-selectors.ts @@ -0,0 +1,30 @@ +// 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 { DatasourceSelector } from "@perses-dev/spec"; +import { DatasourceSelectValue, isVariableDatasource } from "@perses-dev/plugin-system"; +import { DATASOURCE_KIND } from "./constants"; + +export interface JsonDatasourceSelector extends DatasourceSelector { + kind: typeof DATASOURCE_KIND; +} + +export function isDefaultJsonDatasourceSelector(datasourceSelectValue: DatasourceSelectValue): boolean { + return !isVariableDatasource(datasourceSelectValue) && datasourceSelectValue.name === undefined; +} + +export function isJsonDatasourceSelector( + datasourceSelectValue: DatasourceSelectValue +): datasourceSelectValue is JsonDatasourceSelector { + return isVariableDatasource(datasourceSelectValue) || datasourceSelectValue.kind === DATASOURCE_KIND; +} diff --git a/json/src/queries/json-query/json-query-types.ts b/json/src/queries/json-query/json-query-types.ts new file mode 100644 index 000000000..3f4ac282d --- /dev/null +++ b/json/src/queries/json-query/json-query-types.ts @@ -0,0 +1,25 @@ +// 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 { DatasourceSelector } from "@perses-dev/spec"; + +export type HttpMethod = 'GET' | 'POST'; + +export interface JsonQuerySpec { + endpointUrl: string; + method?: HttpMethod; + queryParams?: Record; + body?: string; + jsonataExpression?: string; + datasource?: DatasourceSelector; +} diff --git a/json/src/setup-tests.ts b/json/src/setup-tests.ts new file mode 100644 index 000000000..15db46d31 --- /dev/null +++ b/json/src/setup-tests.ts @@ -0,0 +1,4 @@ +import "@testing-library/jest-dom"; + +// Always mock e-charts during tests since we don't have a proper canvas in jsdom +jest.mock('echarts/core'); diff --git a/json/tsconfig.build.json b/json/tsconfig.build.json new file mode 100644 index 000000000..fc0aafe27 --- /dev/null +++ b/json/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.stories.*", "**/*.test.*", "**/*.map"], + "compilerOptions": { + "emitDeclarationOnly": true, + "declaration": true, + "preserveWatchOutput": true + } +} diff --git a/json/tsconfig.json b/json/tsconfig.json new file mode 100644 index 000000000..40e3c4dfe --- /dev/null +++ b/json/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "outDir": "./dist/lib", + "rootDir": "./src", + "target": "es2022", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "jsx": "react-jsx", + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noUncheckedIndexedAccess": true, + "declaration": true, + "declarationMap": true, + "pretty": true + }, + "include": ["src"] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cb83530be..51d3ffb70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "heatmapchart", "histogramchart", "jaeger", + "json", "logexplorer", "logstable", "loki", @@ -323,6 +324,113 @@ "use-resize-observer": "^9.0.0" } }, + "json": { + "name": "@perses-dev/json", + "version": "0.1.0", + "dependencies": { + "jsonata": "^2.0.5" + }, + "devDependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@hookform/resolvers": "^3.10.0", + "@tanstack/react-query": "^4.44.0", + "@tanstack/react-table": "^8.21.3", + "@uiw/react-codemirror": "^4.25.11", + "date-fns-tz": "^3.2.0", + "echarts": "^5.5.0", + "lodash": "^4.18.1", + "react-hook-form": "^7.85.0", + "use-resize-observer": "^9.1.0" + }, + "peerDependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@perses-dev/client": "^0.54.0", + "@perses-dev/components": "^0.54.0", + "@perses-dev/dashboards": "^0.54.0", + "@perses-dev/explore": "^0.54.0", + "@perses-dev/plugin-system": "^0.54.0", + "@perses-dev/spec": "file:../../spec", + "@tanstack/react-query": "^4.39.1", + "date-fns": "^4.1.0", + "lodash": "^4.17.21", + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0" + } + }, + "json/node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.11", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.11.tgz", + "integrity": "sha512-otyFa+n9IOYtEjaKOxPedHkj15fTPUF21wdR9pv0GpZPfuGl27cvmcv6+tognbRu9VvEcsHKE+ESoszeo3KfTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "json/node_modules/@uiw/react-codemirror": { + "version": "4.25.11", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.11.tgz", + "integrity": "sha512-DYVFAKLX+F/4JS9N/7xexh+TICrlncwkX9HKKInrP1bwO0tSfc3k0GB6oawTYhelVKh20cX3TuRx+NJSkVXuMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.11", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "json/node_modules/react-hook-form": { + "version": "7.85.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.85.0.tgz", + "integrity": "sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "logexplorer": { "name": "@perses-dev/log-explorer-plugin", "version": "0.1.0", @@ -508,9 +616,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -528,9 +633,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -548,9 +650,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -568,9 +667,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -633,6 +729,7 @@ }, "node_modules/@atlaskit/pragmatic-drag-and-drop": { "version": "1.8.1", + "dev": true, "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.0.0", @@ -642,6 +739,7 @@ }, "node_modules/@atlaskit/pragmatic-drag-and-drop-hitbox": { "version": "1.2.0", + "dev": true, "license": "Apache-2.0", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.8.0", @@ -650,6 +748,7 @@ }, "node_modules/@babel/code-frame": { "version": "7.29.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -712,6 +811,7 @@ }, "node_modules/@babel/generator": { "version": "7.29.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", @@ -749,6 +849,7 @@ }, "node_modules/@babel/helper-globals": { "version": "7.29.7", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -756,6 +857,7 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -791,6 +893,7 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -798,6 +901,7 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -825,6 +929,7 @@ }, "node_modules/@babel/parser": { "version": "7.29.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" @@ -1050,6 +1155,7 @@ }, "node_modules/@babel/template": { "version": "7.29.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -1062,6 +1168,7 @@ }, "node_modules/@babel/traverse": { "version": "7.29.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -1078,6 +1185,7 @@ }, "node_modules/@babel/types": { "version": "7.29.7", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -1113,6 +1221,7 @@ }, "node_modules/@codemirror/commands": { "version": "6.10.4", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -1123,6 +1232,7 @@ }, "node_modules/@codemirror/lang-json": { "version": "6.0.2", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -1143,6 +1253,7 @@ }, "node_modules/@codemirror/lint": { "version": "6.9.7", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -1152,6 +1263,7 @@ }, "node_modules/@codemirror/search": { "version": "6.7.1", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -1168,6 +1280,7 @@ }, "node_modules/@codemirror/theme-one-dark": { "version": "6.1.3", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -1313,10 +1426,12 @@ }, "node_modules/@date-fns/tz": { "version": "1.5.0", + "dev": true, "license": "MIT" }, "node_modules/@emnapi/core": { "version": "1.11.1", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1326,6 +1441,7 @@ }, "node_modules/@emnapi/runtime": { "version": "1.11.1", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1334,6 +1450,7 @@ }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1342,8 +1459,8 @@ }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", @@ -1360,8 +1477,8 @@ }, "node_modules/@emotion/cache": { "version": "11.14.0", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", @@ -1372,26 +1489,26 @@ }, "node_modules/@emotion/hash": { "version": "0.9.2", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { "version": "1.4.0", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0" } }, "node_modules/@emotion/memoize": { "version": "0.9.0", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@emotion/react": { "version": "11.14.0", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1413,8 +1530,8 @@ }, "node_modules/@emotion/serialize": { "version": "1.3.3", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", @@ -1425,13 +1542,13 @@ }, "node_modules/@emotion/sheet": { "version": "1.4.0", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@emotion/styled": { "version": "11.14.1", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1452,26 +1569,26 @@ }, "node_modules/@emotion/unitless": { "version": "0.10.0", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.2.0", + "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@emotion/utils": { "version": "1.4.2", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", @@ -1558,6 +1675,7 @@ }, "node_modules/@fontsource/inter": { "version": "5.2.8", + "dev": true, "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -1579,8 +1697,8 @@ }, "node_modules/@hookform/resolvers": { "version": "3.10.0", + "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "react-hook-form": "^7.0.0" } @@ -2149,6 +2267,7 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2166,6 +2285,7 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2173,10 +2293,12 @@ }, "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", @@ -2185,6 +2307,7 @@ }, "node_modules/@juggle/resize-observer": { "version": "3.4.0", + "dev": true, "license": "Apache-2.0" }, "node_modules/@keyv/serialize": { @@ -2205,6 +2328,7 @@ }, "node_modules/@lezer/json": { "version": "1.0.3", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2318,7 +2442,7 @@ }, "node_modules/@module-federation/error-codes": { "version": "2.6.0", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@module-federation/inject-external-runtime-core-plugin": { @@ -2420,7 +2544,7 @@ }, "node_modules/@module-federation/runtime": { "version": "2.6.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@module-federation/error-codes": "2.6.0", @@ -2430,7 +2554,7 @@ }, "node_modules/@module-federation/runtime-core": { "version": "2.6.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@module-federation/error-codes": "2.6.0", @@ -2439,7 +2563,7 @@ }, "node_modules/@module-federation/runtime-tools": { "version": "2.6.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@module-federation/runtime": "2.6.0", @@ -2448,7 +2572,7 @@ }, "node_modules/@module-federation/sdk": { "version": "2.6.0", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "node-fetch": "^2.7.0 || ^3.3.2" @@ -2470,7 +2594,7 @@ }, "node_modules/@module-federation/webpack-bundler-runtime": { "version": "2.6.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@module-federation/error-codes": "2.6.0", @@ -2478,161 +2602,6 @@ "@module-federation/sdk": "2.6.0" } }, - "node_modules/@mui/core-downloads-tracker": { - "version": "6.5.0", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/material": { - "version": "6.5.0", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.5.0", - "@mui/system": "^6.5.0", - "@mui/types": "~7.2.24", - "@mui/utils": "^6.4.9", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^19.0.0", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.5.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "6.4.9", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.4.9", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "6.5.0", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.26.0", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "6.5.0", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.4.9", - "@mui/styled-engine": "^6.5.0", - "@mui/types": "~7.2.24", - "@mui/utils": "^6.4.9", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, "node_modules/@mui/types": { "version": "7.2.24", "license": "MIT", @@ -2711,6 +2680,7 @@ }, "node_modules/@mui/x-date-pickers": { "version": "7.29.4", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -2931,9 +2901,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2951,9 +2918,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2971,9 +2935,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2991,9 +2952,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3011,9 +2969,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3031,9 +2986,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3051,9 +3003,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3133,6 +3082,7 @@ }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3197,6 +3147,7 @@ }, "node_modules/@perses-dev/client": { "version": "0.55.0-beta.1", + "dev": true, "license": "Apache-2.0", "dependencies": { "@perses-dev/spec": "0.3.0-beta.1", @@ -3208,6 +3159,7 @@ }, "node_modules/@perses-dev/components": { "version": "0.55.0-beta.1", + "dev": true, "license": "Apache-2.0", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.4.0", @@ -3244,6 +3196,7 @@ }, "node_modules/@perses-dev/core": { "version": "0.53.0", + "dev": true, "license": "Apache-2.0", "dependencies": { "date-fns": "^4.1.0", @@ -3255,6 +3208,7 @@ }, "node_modules/@perses-dev/dashboards": { "version": "0.55.0-beta.1", + "dev": true, "license": "Apache-2.0", "dependencies": { "@perses-dev/client": "0.55.0-beta.1", @@ -3287,6 +3241,7 @@ }, "node_modules/@perses-dev/explore": { "version": "0.55.0-beta.1", + "dev": true, "license": "Apache-2.0", "dependencies": { "@nexucis/fuzzy": "^0.5.1", @@ -3330,6 +3285,10 @@ "resolved": "jaeger", "link": true }, + "node_modules/@perses-dev/json": { + "resolved": "json", + "link": true + }, "node_modules/@perses-dev/log-explorer-plugin": { "resolved": "logexplorer", "link": true @@ -3356,6 +3315,7 @@ }, "node_modules/@perses-dev/plugin-system": { "version": "0.55.0-beta.1", + "dev": true, "license": "Apache-2.0", "dependencies": { "@module-federation/enhanced": "^2.8.0", @@ -3383,6 +3343,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/bridge-react-webpack-plugin": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/sdk": "2.8.2" @@ -3390,6 +3351,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/cli": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/dts-plugin": "2.8.2", @@ -3406,6 +3368,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/dts-plugin": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/error-codes": "2.8.2", @@ -3429,6 +3392,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/enhanced": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/bridge-react-webpack-plugin": "2.8.2", @@ -3467,10 +3431,12 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/error-codes": { "version": "2.8.2", + "dev": true, "license": "MIT" }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/inject-external-runtime-core-plugin": { "version": "2.8.2", + "dev": true, "license": "MIT", "peerDependencies": { "@module-federation/runtime-tools": "2.8.2" @@ -3478,6 +3444,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/managers": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/sdk": "2.8.2" @@ -3485,6 +3452,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/manifest": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/dts-plugin": "2.8.2", @@ -3494,6 +3462,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/rspack": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/bridge-react-webpack-plugin": "2.8.2", @@ -3520,6 +3489,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/runtime": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/error-codes": "2.8.2", @@ -3529,6 +3499,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/runtime-core": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/error-codes": "2.8.2", @@ -3537,6 +3508,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/runtime-tools": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/runtime": "2.8.2", @@ -3545,14 +3517,17 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/sdk": { "version": "2.8.2", + "dev": true, "license": "MIT" }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/third-party-dts-extractor": { "version": "2.8.2", + "dev": true, "license": "MIT" }, "node_modules/@perses-dev/plugin-system/node_modules/@module-federation/webpack-bundler-runtime": { "version": "2.8.2", + "dev": true, "license": "MIT", "dependencies": { "@module-federation/error-codes": "2.8.2", @@ -3562,6 +3537,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/adm-zip": { "version": "0.6.0", + "dev": true, "license": "MIT", "engines": { "node": ">=14.0" @@ -3569,6 +3545,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/semver": { "version": "7.8.5", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3579,6 +3556,7 @@ }, "node_modules/@perses-dev/plugin-system/node_modules/undici": { "version": "7.29.0", + "dev": true, "license": "MIT", "engines": { "node": ">=20.18.1" @@ -3602,6 +3580,7 @@ }, "node_modules/@perses-dev/spec": { "version": "0.3.0-beta.1", + "dev": true, "license": "Apache-2.0", "dependencies": { "date-fns": "^4.1.0", @@ -3611,6 +3590,7 @@ }, "node_modules/@perses-dev/spec/node_modules/fraction.js": { "version": "5.3.4", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -3622,6 +3602,7 @@ }, "node_modules/@perses-dev/spec/node_modules/mathjs": { "version": "15.2.0", + "dev": true, "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.26.10", @@ -3643,6 +3624,7 @@ }, "node_modules/@perses-dev/spec/node_modules/typed-function": { "version": "4.2.2", + "dev": true, "license": "MIT", "engines": { "node": ">= 18" @@ -3726,15 +3708,6 @@ "node": ">=18" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@prometheus-io/codemirror-promql": { "version": "0.313.1", "license": "Apache-2.0", @@ -3771,6 +3744,7 @@ }, "node_modules/@remix-run/router": { "version": "1.23.3", + "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -3905,9 +3879,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3922,9 +3893,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3939,9 +3907,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3956,9 +3921,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3981,325 +3943,97 @@ }, "node_modules/@rsbuild/core/node_modules/@rspack/binding-win32-arm64-msvc": { "version": "1.7.12", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.12.tgz", - "integrity": "sha512-8+h5fYDXYdmugbdfZ+D1y8IQ3rv2EhSfyGP7vBe+bjNyaMa4jWrpucmZbtxojUL1AzaeuHbvMdj9UO/gelk/+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rsbuild/core/node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "1.7.12", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.12.tgz", - "integrity": "sha512-cDMGwTRSa2p9fNBVe1wTRkF2AEXZ9ARWW36QeC5CkLaI0Ezz8lvhF2+CSOPnhaQ1O1qtn0L0SF+lFnrY+I7xGQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rsbuild/core/node_modules/@rspack/binding-win32-x64-msvc": { - "version": "1.7.12", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.12.tgz", - "integrity": "sha512-wIqFvlgFqrgUyj/6S/FJcvShnkZOmIeXTfqvheLY67MGq8qd8jb1YimQVKAIrmWB3yuJKUFACI3Ag1UBtEedEA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rsbuild/core/node_modules/@rspack/core": { - "version": "1.7.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/runtime-tools": "0.22.0", - "@rspack/binding": "1.7.12", - "@rspack/lite-tapable": "1.1.0" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.1" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@rsbuild/core/node_modules/@rspack/lite-tapable": { - "version": "1.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@rsbuild/core/node_modules/jiti": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/@rsbuild/plugin-react": { - "version": "1.4.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@rspack/plugin-react-refresh": "^1.6.1", - "react-refresh": "^0.18.0" - }, - "peerDependencies": { - "@rsbuild/core": "^1.0.0 || ^2.0.0-0" - }, - "peerDependenciesMeta": { - "@rsbuild/core": { - "optional": true - } - } - }, - "node_modules/@rspack/binding": { - "version": "2.1.2", - "license": "MIT", - "peer": true, - "optionalDependencies": { - "@rspack/binding-darwin-arm64": "2.1.2", - "@rspack/binding-darwin-x64": "2.1.2", - "@rspack/binding-linux-arm64-gnu": "2.1.2", - "@rspack/binding-linux-arm64-musl": "2.1.2", - "@rspack/binding-linux-riscv64-gnu": "2.1.2", - "@rspack/binding-linux-riscv64-musl": "2.1.2", - "@rspack/binding-linux-x64-gnu": "2.1.2", - "@rspack/binding-linux-x64-musl": "2.1.2", - "@rspack/binding-wasm32-wasi": "2.1.2", - "@rspack/binding-win32-arm64-msvc": "2.1.2", - "@rspack/binding-win32-ia32-msvc": "2.1.2", - "@rspack/binding-win32-x64-msvc": "2.1.2" - } - }, - "node_modules/@rspack/binding-darwin-arm64": { - "version": "2.1.2", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@rspack/binding-darwin-x64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.1.2.tgz", - "integrity": "sha512-aoifkILvx/XEHyvg8yW57xu95nx7f9f/3ah1+RguHSNKcJMcoCep9VX1Ct1N0ftqg8MC0JUObc7xWL5W14hmjA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-arm64-gnu": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.1.2.tgz", - "integrity": "sha512-My4m40tyJSgiCEf3bB2KIEX710q3nZg99LIjy+8Zxgi3oZTkg1bFmFRusFU5U4eN5408zfSqDDGvjDE3Yv7o4w==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-arm64-musl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.1.2.tgz", - "integrity": "sha512-yt+GGWUH7WPE8K97cRc8OpZhH7Pbj1vU+lkvKbDtF/rR8X9a/bJsA/nBqyUV2oBKOVbrp5I8rFZlnDskMqgvKw==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-riscv64-gnu": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-2.1.2.tgz", - "integrity": "sha512-uys8Jyw8Z3ralvICbN/L/nZfy5qELIwpOY72rhIqhoDYwFcL4fmMaY7WsvUcJOjCB2rqOcWPaWKuF2oPvo9iDQ==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-riscv64-musl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-2.1.2.tgz", - "integrity": "sha512-JYNVQwqCaRGQWvjHQYzZkIzQiwllMaJwh4Rdu3ww6W2OJcJUqT08sL1pkOtU0iCxT4VUYiRRcp93VGTGpHr8fg==", - "cpu": [ - "riscv64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-x64-gnu": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.1.2.tgz", - "integrity": "sha512-KDoPy0Msf/JLhxgPPrJQzZeB4Qpqd32em8AP5lSW2s6jR5I35dHgAe9xc2A++EQtnSrU4GTn6DBvFC7q84SihQ==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-x64-musl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.1.2.tgz", - "integrity": "sha512-66hWmIGvn4zCKAYXJE9Bp5SNSLYnLFq2Ke/efE+ZtWy43Dd5vk9AAOmThVGBwdwmIxmGtHGCp+cAuS4G0wu0TA==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-wasm32-wasi": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.1.2.tgz", - "integrity": "sha512-EB4SqH8DW/E/OmqssNQvnIVGQiVUyYNlA/pcc6Ia4MlTNwu6eNDppcNLrToH+kSZpL4CpHSFfSM3eIsSuar2Rw==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "1.1.6" - } - }, - "node_modules/@rspack/binding-win32-arm64-msvc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.1.2.tgz", - "integrity": "sha512-T6Fs/g32MRja/UpCq4AdyPRj8tA0cOkcEa4PrAcn/ztUgK8b/qMVxj5mhMI+n7k+kHZQnpeB1Q4HqdSJi6OocA==", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.12.tgz", + "integrity": "sha512-8+h5fYDXYdmugbdfZ+D1y8IQ3rv2EhSfyGP7vBe+bjNyaMa4jWrpucmZbtxojUL1AzaeuHbvMdj9UO/gelk/+g==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ], - "peer": true + ] }, - "node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.1.2.tgz", - "integrity": "sha512-OtxkFVz14mVL4QK8QriSELn9B6PaYGHw1jGJwVDEzpu2ZxSHCTQPz9dVE1ekYtREEqZUkRU7Fp7VfhJSmjTt2Q==", + "node_modules/@rsbuild/core/node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.12.tgz", + "integrity": "sha512-cDMGwTRSa2p9fNBVe1wTRkF2AEXZ9ARWW36QeC5CkLaI0Ezz8lvhF2+CSOPnhaQ1O1qtn0L0SF+lFnrY+I7xGQ==", "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ], - "peer": true + ] }, - "node_modules/@rspack/binding-win32-x64-msvc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.1.2.tgz", - "integrity": "sha512-Am+nx9fLF3nzgD/K05Bs1Bb+WO8SFLWAYRbXkymaL1r+RQxjRj7jd5ap2PhGOCcfaNA4yVWkAFvmFP92eRu7bQ==", + "node_modules/@rsbuild/core/node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.12.tgz", + "integrity": "sha512-wIqFvlgFqrgUyj/6S/FJcvShnkZOmIeXTfqvheLY67MGq8qd8jb1YimQVKAIrmWB3yuJKUFACI3Ag1UBtEedEA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ], - "peer": true + ] }, - "node_modules/@rspack/core": { - "version": "2.1.2", + "node_modules/@rsbuild/core/node_modules/@rspack/core": { + "version": "1.7.12", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@rspack/binding": "2.1.2" + "@module-federation/runtime-tools": "0.22.0", + "@rspack/binding": "1.7.12", + "@rspack/lite-tapable": "1.1.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.12.0" }, "peerDependencies": { - "@module-federation/runtime-tools": "^0.24.1 || ^2.0.0", - "@swc/helpers": "^0.5.23" + "@swc/helpers": ">=0.5.1" }, "peerDependenciesMeta": { - "@module-federation/runtime-tools": { - "optional": true - }, "@swc/helpers": { "optional": true } } }, + "node_modules/@rsbuild/core/node_modules/@rspack/lite-tapable": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@rsbuild/core/node_modules/jiti": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@rsbuild/plugin-react": { + "version": "1.4.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@rspack/plugin-react-refresh": "^1.6.1", + "react-refresh": "^0.18.0" + }, + "peerDependencies": { + "@rsbuild/core": "^1.0.0 || ^2.0.0-0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + } + } + }, "node_modules/@rspack/plugin-react-refresh": { "version": "1.6.2", "dev": true, @@ -4515,9 +4249,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4535,9 +4266,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4555,9 +4283,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4575,9 +4300,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4595,9 +4317,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4615,9 +4334,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4685,7 +4401,7 @@ }, "node_modules/@swc/helpers": { "version": "0.5.23", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -4717,6 +4433,7 @@ }, "node_modules/@tanstack/hotkeys": { "version": "0.8.0", + "dev": true, "license": "MIT", "dependencies": { "@tanstack/store": "^0.11.0" @@ -4731,6 +4448,7 @@ }, "node_modules/@tanstack/match-sorter-utils": { "version": "8.19.4", + "dev": true, "license": "MIT", "dependencies": { "remove-accents": "0.5.0" @@ -4745,8 +4463,8 @@ }, "node_modules/@tanstack/query-core": { "version": "4.44.0", + "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -4754,6 +4472,7 @@ }, "node_modules/@tanstack/react-hotkeys": { "version": "0.9.1", + "dev": true, "license": "MIT", "dependencies": { "@tanstack/hotkeys": "0.7.1", @@ -4773,6 +4492,7 @@ }, "node_modules/@tanstack/react-hotkeys/node_modules/@tanstack/hotkeys": { "version": "0.7.1", + "dev": true, "license": "MIT", "dependencies": { "@tanstack/store": "^0.9.3" @@ -4787,6 +4507,7 @@ }, "node_modules/@tanstack/react-hotkeys/node_modules/@tanstack/store": { "version": "0.9.3", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -4795,8 +4516,8 @@ }, "node_modules/@tanstack/react-query": { "version": "4.44.0", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "4.44.0", "use-sync-external-store": "^1.6.0" @@ -4821,6 +4542,7 @@ }, "node_modules/@tanstack/react-store": { "version": "0.9.3", + "dev": true, "license": "MIT", "dependencies": { "@tanstack/store": "0.9.3", @@ -4837,6 +4559,7 @@ }, "node_modules/@tanstack/react-store/node_modules/@tanstack/store": { "version": "0.9.3", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -4845,6 +4568,7 @@ }, "node_modules/@tanstack/react-table": { "version": "8.21.3", + "dev": true, "license": "MIT", "dependencies": { "@tanstack/table-core": "8.21.3" @@ -4863,6 +4587,7 @@ }, "node_modules/@tanstack/store": { "version": "0.11.0", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -4871,6 +4596,7 @@ }, "node_modules/@tanstack/table-core": { "version": "8.21.3", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5087,6 +4813,7 @@ }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5260,6 +4987,7 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", + "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -5282,8 +5010,8 @@ }, "node_modules/@types/parse-json": { "version": "4.0.2", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.15", @@ -5299,25 +5027,9 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/react": { - "version": "18.3.31", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "license": "MIT", - "peer": true, - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.12", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*" @@ -5400,30 +5112,6 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.62.1", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, "node_modules/@typescript-eslint/project-service": { "version": "8.62.1", "dev": true, @@ -5631,6 +5319,7 @@ }, "node_modules/@uiw/codemirror-extensions-basic-setup": { "version": "4.25.10", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -5656,6 +5345,7 @@ }, "node_modules/@uiw/react-codemirror": { "version": "4.25.10", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.6", @@ -5787,9 +5477,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5804,9 +5491,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5821,9 +5505,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5838,9 +5519,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5855,9 +5533,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5872,9 +5547,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5889,9 +5561,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5906,9 +5575,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5923,9 +5589,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5940,9 +5603,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6268,6 +5928,7 @@ }, "node_modules/ajv-formats": { "version": "2.1.1", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -6283,6 +5944,7 @@ }, "node_modules/ajv-formats/node_modules/ajv": { "version": "8.20.0", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -6297,6 +5959,7 @@ }, "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", + "dev": true, "license": "MIT" }, "node_modules/ansi_up": { @@ -6645,8 +6308,8 @@ }, "node_modules/babel-plugin-macros": { "version": "3.1.0", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -6747,6 +6410,7 @@ }, "node_modules/bignumber.js": { "version": "9.3.1", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -6873,6 +6537,7 @@ }, "node_modules/bind-event-listener": { "version": "3.0.0", + "dev": true, "license": "MIT" }, "node_modules/body-parser": { @@ -7094,6 +6759,7 @@ }, "node_modules/callsites": { "version": "3.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7276,6 +6942,7 @@ }, "node_modules/codemirror": { "version": "6.0.2", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -7314,6 +6981,7 @@ }, "node_modules/commander": { "version": "11.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -7321,6 +6989,7 @@ }, "node_modules/complex.js": { "version": "2.4.3", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -7413,8 +7082,8 @@ }, "node_modules/convert-source-map": { "version": "1.9.0", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/cookie": { "version": "0.7.2", @@ -7441,8 +7110,8 @@ }, "node_modules/cosmiconfig": { "version": "7.1.0", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -7456,8 +7125,8 @@ }, "node_modules/cosmiconfig/node_modules/yaml": { "version": "1.10.3", + "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">= 6" } @@ -7531,6 +7200,7 @@ }, "node_modules/csstype": { "version": "3.2.3", + "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -7540,7 +7210,7 @@ }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -7608,6 +7278,7 @@ }, "node_modules/date-fns": { "version": "4.4.0", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -7616,6 +7287,7 @@ }, "node_modules/date-fns-tz": { "version": "3.2.0", + "dev": true, "license": "MIT", "peerDependencies": { "date-fns": "^3.0.0 || ^4.0.0" @@ -7623,6 +7295,7 @@ }, "node_modules/debug": { "version": "4.4.3", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7638,6 +7311,7 @@ }, "node_modules/decimal.js": { "version": "10.6.0", + "dev": true, "license": "MIT" }, "node_modules/decompress-response": { @@ -7771,6 +7445,7 @@ }, "node_modules/dom-helpers": { "version": "5.2.1", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", @@ -7803,6 +7478,7 @@ }, "node_modules/echarts": { "version": "5.5.0", + "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "2.3.0", @@ -7811,6 +7487,7 @@ }, "node_modules/echarts/node_modules/tslib": { "version": "2.3.0", + "dev": true, "license": "0BSD" }, "node_modules/ee-first": { @@ -7868,6 +7545,7 @@ }, "node_modules/error-ex": { "version": "1.3.4", + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -8074,10 +7752,12 @@ }, "node_modules/escape-latex": { "version": "1.2.0", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -8772,6 +8452,7 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -8781,6 +8462,7 @@ }, "node_modules/fast-equals": { "version": "4.0.3", + "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -8800,6 +8482,7 @@ }, "node_modules/fast-uri": { "version": "3.1.3", + "dev": true, "funding": [ { "type": "github", @@ -8846,7 +8529,7 @@ }, "node_modules/fetch-blob": { "version": "3.2.0", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -8987,8 +8670,8 @@ }, "node_modules/find-root": { "version": "1.1.0", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", @@ -9077,7 +8760,7 @@ }, "node_modules/formdata-polyfill": { "version": "4.0.10", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" @@ -9096,6 +8779,7 @@ }, "node_modules/fraction.js": { "version": "4.3.7", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -9390,6 +9074,7 @@ }, "node_modules/goober": { "version": "2.1.19", + "dev": true, "license": "MIT", "peerDependencies": { "csstype": "^3.0.10" @@ -9539,16 +9224,16 @@ }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", + "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "react-is": "^16.7.0" } }, "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/homedir-polyfill": { "version": "1.0.3", @@ -9685,6 +9370,7 @@ }, "node_modules/immer": { "version": "10.2.0", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -9693,6 +9379,7 @@ }, "node_modules/import-fresh": { "version": "3.3.1", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -9805,6 +9492,7 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", + "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -9867,6 +9555,7 @@ }, "node_modules/is-core-module": { "version": "2.16.2", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.3" @@ -10223,6 +9912,7 @@ }, "node_modules/isomorphic-ws": { "version": "5.0.0", + "dev": true, "license": "MIT", "peerDependencies": { "ws": "*" @@ -10321,6 +10011,7 @@ }, "node_modules/javascript-natural-sort": { "version": "0.7.1", + "dev": true, "license": "MIT" }, "node_modules/jest": { @@ -11278,6 +10969,7 @@ }, "node_modules/jiti": { "version": "2.4.2", + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -11348,6 +11040,7 @@ }, "node_modules/jsesc": { "version": "3.1.0", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -11363,6 +11056,7 @@ }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -11386,6 +11080,15 @@ "node": ">=6" } }, + "node_modules/jsonata": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.2.2.tgz", + "integrity": "sha512-XDFH2PuaAXv0AXJEWwElXbqADmKFGKMLMkFb+qOz0EhjQW2EIJ6pgtordLU+4u3IVERyBfqy6bi6kZKEncvRqA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/jsonc-parser": { "version": "3.3.1", "dev": true, @@ -11459,6 +11162,7 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -11477,6 +11181,7 @@ }, "node_modules/lodash": { "version": "4.18.1", + "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -11520,7 +11225,7 @@ }, "node_modules/luxon": { "version": "3.7.2", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -11607,6 +11312,7 @@ }, "node_modules/mathjs": { "version": "10.6.4", + "dev": true, "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.18.6", @@ -11628,6 +11334,7 @@ }, "node_modules/mdi-material-ui": { "version": "7.9.4", + "dev": true, "license": "MIT", "peerDependencies": { "@mui/material": "^5.0.0 || ^6.0.0 || ^7.0.0", @@ -11763,6 +11470,7 @@ }, "node_modules/ms": { "version": "2.1.3", + "dev": true, "license": "MIT" }, "node_modules/napi-postinstall": { @@ -11794,7 +11502,7 @@ }, "node_modules/node-domexception": { "version": "1.0.0", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -11837,7 +11545,7 @@ }, "node_modules/node-fetch": { "version": "3.3.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -11899,6 +11607,7 @@ }, "node_modules/notistack": { "version": "3.0.2", + "dev": true, "license": "MIT", "dependencies": { "clsx": "^1.1.0", @@ -11919,6 +11628,7 @@ }, "node_modules/notistack/node_modules/clsx": { "version": "1.2.1", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11952,6 +11662,7 @@ }, "node_modules/numbro": { "version": "2.5.0", + "dev": true, "license": "MIT", "dependencies": { "bignumber.js": "^8 || ^9" @@ -12211,6 +11922,7 @@ }, "node_modules/parent-module": { "version": "1.0.1", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -12221,6 +11933,7 @@ }, "node_modules/parse-json": { "version": "5.2.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -12299,6 +12012,7 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -12328,8 +12042,8 @@ }, "node_modules/path-type": { "version": "4.0.0", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -12341,6 +12055,7 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -12485,21 +12200,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.9.4", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/prettier-linter-helpers": { "version": "1.0.1", "dev": true, @@ -12648,6 +12348,7 @@ }, "node_modules/raf-schd": { "version": "4.0.3", + "dev": true, "license": "MIT" }, "node_modules/range-parser": { @@ -12685,6 +12386,7 @@ }, "node_modules/react": { "version": "18.3.1", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -12695,6 +12397,7 @@ }, "node_modules/react-colorful": { "version": "5.7.0", + "dev": true, "license": "MIT", "peerDependencies": { "react": ">=16.8.0", @@ -12703,6 +12406,7 @@ }, "node_modules/react-dom": { "version": "18.3.1", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -12714,6 +12418,7 @@ }, "node_modules/react-draggable": { "version": "4.7.0", + "dev": true, "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -12726,6 +12431,7 @@ }, "node_modules/react-error-boundary": { "version": "3.1.4", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" @@ -12740,6 +12446,7 @@ }, "node_modules/react-grid-layout": { "version": "1.5.3", + "dev": true, "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -12756,6 +12463,7 @@ }, "node_modules/react-hook-form": { "version": "7.80.0", + "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -12770,6 +12478,7 @@ }, "node_modules/react-intersection-observer": { "version": "9.16.0", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -12807,6 +12516,7 @@ }, "node_modules/react-resizable": { "version": "3.2.0", + "dev": true, "license": "MIT", "dependencies": { "prop-types": "15.x", @@ -12819,6 +12529,7 @@ }, "node_modules/react-router": { "version": "6.30.4", + "dev": true, "license": "MIT", "dependencies": { "@remix-run/router": "1.23.3" @@ -12832,6 +12543,7 @@ }, "node_modules/react-router-dom": { "version": "6.30.4", + "dev": true, "license": "MIT", "dependencies": { "@remix-run/router": "1.23.3", @@ -12847,6 +12559,7 @@ }, "node_modules/react-transition-group": { "version": "4.4.5", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", @@ -12921,6 +12634,7 @@ }, "node_modules/remove-accents": { "version": "0.5.0", + "dev": true, "license": "MIT" }, "node_modules/require-directory": { @@ -12933,6 +12647,7 @@ }, "node_modules/require-from-string": { "version": "2.0.2", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12944,10 +12659,12 @@ }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", + "dev": true, "license": "MIT" }, "node_modules/resolve": { "version": "1.22.8", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -12999,6 +12716,7 @@ }, "node_modules/resolve-from": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -13229,6 +12947,7 @@ }, "node_modules/scheduler": { "version": "0.23.2", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -13236,6 +12955,7 @@ }, "node_modules/schema-utils": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -13253,6 +12973,7 @@ }, "node_modules/schema-utils/node_modules/ajv": { "version": "8.20.0", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -13267,6 +12988,7 @@ }, "node_modules/schema-utils/node_modules/ajv-keywords": { "version": "5.1.0", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" @@ -13277,10 +12999,12 @@ }, "node_modules/schema-utils/node_modules/json-schema-traverse": { "version": "1.0.0", + "dev": true, "license": "MIT" }, "node_modules/seedrandom": { "version": "3.0.5", + "dev": true, "license": "MIT" }, "node_modules/seek-bzip": { @@ -13377,6 +13101,7 @@ }, "node_modules/serialize-query-params": { "version": "2.0.4", + "dev": true, "license": "ISC" }, "node_modules/serve-static": { @@ -13591,8 +13316,8 @@ }, "node_modules/source-map": { "version": "0.5.7", + "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13949,8 +13674,8 @@ }, "node_modules/stylis": { "version": "4.2.0", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/super-regex": { "version": "1.1.0", @@ -13981,6 +13706,7 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -14021,6 +13747,7 @@ }, "node_modules/tapable": { "version": "2.3.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -14152,6 +13879,7 @@ }, "node_modules/tiny-emitter": { "version": "2.1.0", + "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { @@ -14330,7 +14058,7 @@ }, "node_modules/tslib": { "version": "2.8.1", - "devOptional": true, + "dev": true, "license": "0BSD" }, "node_modules/turbo": { @@ -14463,12 +14191,14 @@ }, "node_modules/typed-function": { "version": "2.1.0", + "dev": true, "engines": { "node": ">= 10" } }, "node_modules/typescript": { "version": "5.9.3", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14631,6 +14361,7 @@ }, "node_modules/use-immer": { "version": "0.11.0", + "dev": true, "license": "MIT", "peerDependencies": { "immer": ">=8.0.0", @@ -14639,6 +14370,7 @@ }, "node_modules/use-query-params": { "version": "2.2.2", + "dev": true, "license": "ISC", "dependencies": { "serialize-query-params": "^2.0.3" @@ -14660,6 +14392,7 @@ }, "node_modules/use-resize-observer": { "version": "9.1.0", + "dev": true, "license": "MIT", "dependencies": { "@juggle/resize-observer": "^3.3.1" @@ -14740,7 +14473,7 @@ }, "node_modules/web-streams-polyfill": { "version": "3.3.3", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -15004,6 +14737,7 @@ }, "node_modules/ws": { "version": "8.21.0", + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -15049,6 +14783,7 @@ }, "node_modules/yaml": { "version": "2.9.0", + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -15173,6 +14908,7 @@ }, "node_modules/zod": { "version": "3.25.76", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -15180,6 +14916,7 @@ }, "node_modules/zrender": { "version": "5.5.0", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "tslib": "2.3.0" @@ -15187,10 +14924,12 @@ }, "node_modules/zrender/node_modules/tslib": { "version": "2.3.0", + "dev": true, "license": "0BSD" }, "node_modules/zustand": { "version": "4.5.7", + "dev": true, "license": "MIT", "dependencies": { "use-sync-external-store": "^1.2.2" diff --git a/package.json b/package.json index 812c9f3b1..7a034f37f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "heatmapchart", "histogramchart", "jaeger", + "json", "logexplorer", "logstable", "loki", diff --git a/table/src/Table.ts b/table/src/Table.ts index 0c9df73a3..577c3d1a1 100644 --- a/table/src/Table.ts +++ b/table/src/Table.ts @@ -30,7 +30,7 @@ import { TableExportAction } from './TableExportAction'; */ export const Table: PanelPlugin = { PanelComponent: TablePanel, - supportedQueryTypes: ['TimeSeriesQuery'], + supportedQueryTypes: ['TimeSeriesQuery', 'JsonQuery'], queryOptions: getTablePanelQueryOptions, panelOptionsEditorComponents: [ { label: 'General Settings', content: TableSettingsEditor }, diff --git a/table/src/TableExportAction.tsx b/table/src/TableExportAction.tsx index 4bbea6d42..85bb719d6 100644 --- a/table/src/TableExportAction.tsx +++ b/table/src/TableExportAction.tsx @@ -16,7 +16,7 @@ import { escapeCsvValue, PanelData, sanitizeFilename } from '@perses-dev/plugin- import { InfoTooltip, transformData } from '@perses-dev/components'; import { IconButton } from '@mui/material'; import DownloadIcon from 'mdi-material-ui/Download'; -import { TimeSeriesData } from '@perses-dev/spec'; +import { QueryDataType } from '@perses-dev/spec'; import { TableProps } from './components'; import type { TableOptions } from './models'; import { buildRawTableData } from './table-data-utils'; @@ -32,10 +32,9 @@ export interface ExportColumn { * CSV output matches the visual table. */ export function buildTableData( - queryResults: Array>, + queryResults: Array>, spec: TableOptions ): { data: Array>; columns: ExportColumn[] } { - // Use shared utility with forExport=true to get raw scalar values const rawData = buildRawTableData(queryResults, spec, { forExport: true }); const transformed = transformData(rawData, spec.transforms ?? []); diff --git a/table/src/components/TablePanel.tsx b/table/src/components/TablePanel.tsx index 586e90364..a462d6ed2 100644 --- a/table/src/components/TablePanel.tsx +++ b/table/src/components/TablePanel.tsx @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Box, Theme, Typography, useTheme } from '@mui/material'; +import { Box, Theme, Typography, useTheme } from "@mui/material"; import { FormatOptions, formatValue, @@ -19,9 +19,9 @@ import { TableCellConfigs, TableColumnConfig, transformData, - useSelection, -} from '@perses-dev/components'; -import { useSelectionItemActions } from '@perses-dev/dashboards'; + useSelection +} from "@perses-dev/components"; +import { useSelectionItemActions } from "@perses-dev/dashboards"; import { ActionOptions, CalculationsMap, @@ -29,15 +29,15 @@ import { PanelProps, replaceVariablesInString, useAllVariableValues, - VariableStateMap, -} from '@perses-dev/plugin-system'; -import { ColumnFiltersState, PaginationState, RowSelectionState, SortingState } from '@tanstack/react-table'; -import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { QueryDataType, TimeSeriesData } from '@perses-dev/spec'; -import { createPortal } from 'react-dom'; -import { CellSettings, ColumnSettings, evaluateConditionalFormatting, TableOptions } from '../models'; -import { buildRawTableData, getTablePanelQueryMode } from '../table-data-utils'; -import { EmbeddedPanel } from './EmbeddedPanel'; + VariableStateMap +} from "@perses-dev/plugin-system"; +import { ColumnFiltersState, PaginationState, RowSelectionState, SortingState } from "@tanstack/react-table"; +import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { JsonData, QueryDataType, TimeSeriesData } from "@perses-dev/spec"; +import { createPortal } from "react-dom"; +import { CellSettings, ColumnSettings, evaluateConditionalFormatting, TableOptions } from "../models"; +import { buildRawTableData, getTablePanelQueryMode } from "../table-data-utils"; +import { EmbeddedPanel } from "./EmbeddedPanel"; type FilterValuesType = Array<{ original: T; formatted: T }>; @@ -385,7 +385,7 @@ export function getTablePanelQueryOptions(spec: TableOptions): { mode: 'instant' }; } -export type TableProps = PanelProps; +export type TableProps = PanelProps; export function TablePanel({ contentDimensions, spec, queryResults }: TableProps): ReactElement | null { const theme = useTheme(); @@ -439,9 +439,7 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps [setSelection, clearSelection] ); - // TODO: handle other query types const rawData: Array> = useMemo(() => { - // Transform query results to a tabular format using shared utility return buildRawTableData(queryResults, spec); }, [queryResults, spec]); diff --git a/table/src/table-data-utils.test.ts b/table/src/table-data-utils.test.ts new file mode 100644 index 000000000..2bd1dac06 --- /dev/null +++ b/table/src/table-data-utils.test.ts @@ -0,0 +1,244 @@ +// 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 { PanelData } from '@perses-dev/plugin-system'; +import { JsonData, TimeSeriesData } from '@perses-dev/spec'; +import { buildJsonTableData, buildRawTableData, getTablePanelQueryMode } from './table-data-utils'; +import { TableOptions } from './models'; + + +function makeTsPanelData(series: TimeSeriesData['series']): PanelData { + return { + definition: { + kind: 'TimeSeriesQuery', + spec: { plugin: { kind: 'PrometheusTimeSeriesQuery', spec: { query: '' } } }, + }, + data: { + timeRange: { start: new Date(0), end: new Date(0) }, + stepMs: 15000, + series, + }, + }; +} + +function makeJsonPanelData(payload: unknown): PanelData { + return { + definition: { + kind: 'JsonQuery', + spec: { plugin: { kind: 'JsonQuery', spec: {} } }, + }, + data: { data: payload }, + }; +} + + +describe('getTablePanelQueryMode', () => { + it('returns instant when there are no column settings', () => { + expect(getTablePanelQueryMode({})).toBe('instant'); + }); + + it('returns instant when column settings have no plugin', () => { + const spec: TableOptions = { columnSettings: [{ name: 'value' }, { name: 'host' }] }; + expect(getTablePanelQueryMode(spec)).toBe('instant'); + }); + + it('returns range when any column has a plugin', () => { + const spec: TableOptions = { + columnSettings: [{ name: 'value', plugin: { kind: 'StatChart', spec: {} } }], + }; + expect(getTablePanelQueryMode(spec)).toBe('range'); + }); + + it('returns range when only one of many columns has a plugin', () => { + const spec: TableOptions = { + columnSettings: [ + { name: 'host' }, + { name: 'value', plugin: { kind: 'StatChart', spec: {} } }, + { name: 'env' }, + ], + }; + expect(getTablePanelQueryMode(spec)).toBe('range'); + }); +}); + + +describe('buildJsonTableData', () => { + it('returns empty array for no results', () => { + expect(buildJsonTableData([])).toEqual([]); + }); + + it('maps an object payload to a single row', () => { + const rows = buildJsonTableData([makeJsonPanelData({ name: 'Alice', age: 30 })]); + expect(rows).toEqual([{ name: 'Alice', age: 30 }]); + }); + + it('wraps a numeric primitive in { value }', () => { + expect(buildJsonTableData([makeJsonPanelData(42)])).toEqual([{ value: 42 }]); + }); + + it('wraps a string primitive in { value }', () => { + expect(buildJsonTableData([makeJsonPanelData('hello')])).toEqual([{ value: 'hello' }]); + }); + + it('wraps a boolean primitive in { value }', () => { + expect(buildJsonTableData([makeJsonPanelData(true)])).toEqual([{ value: true }]); + }); + + it('wraps null in { value: null }', () => { + expect(buildJsonTableData([makeJsonPanelData(null)])).toEqual([{ value: null }]); + }); + + it('JSON-stringifies nested object values', () => { + const rows = buildJsonTableData([makeJsonPanelData({ meta: { env: 'prod' }, count: 1 })]); + expect(rows).toEqual([{ meta: '{"env":"prod"}', count: 1 }]); + }); + + it('JSON-stringifies nested array values', () => { + const rows = buildJsonTableData([makeJsonPanelData({ tags: ['a', 'b'] })]); + expect(rows).toEqual([{ tags: '["a","b"]' }]); + }); + + it('leaves scalar values on object rows as-is', () => { + const rows = buildJsonTableData([makeJsonPanelData({ score: 99, active: false })]); + expect(rows).toEqual([{ score: 99, active: false }]); + }); + + it('produces one row per query result when each carries a single object', () => { + const rows = buildJsonTableData([ + makeJsonPanelData({ id: 1 }), + makeJsonPanelData({ id: 2 }), + ]); + expect(rows).toEqual([{ id: 1 }, { id: 2 }]); + }); + + it('treats an array payload as a single row using numeric index keys (stringifying objects)', () => { + const rows = buildJsonTableData([makeJsonPanelData([{ a: 1 }, { b: 2 }])]); + expect(rows).toEqual([{ '0': '{"a":1}', '1': '{"b":2}' }]); + }); + + it('treats an array of primitives as a single row with numeric index keys', () => { + const rows = buildJsonTableData([makeJsonPanelData([10, 20, 30])]); + expect(rows).toEqual([{ '0': 10, '1': 20, '2': 30 }]); + }); +}); + + +describe('buildRawTableData', () => { + it('returns empty array when there are no results', () => { + expect(buildRawTableData([], {})).toEqual([]); + }); + + it('routes TimeSeriesQuery results to the time-series path', () => { + const result = buildRawTableData( + [makeTsPanelData([{ name: 's', values: [[1000, 42]], labels: { host: 'a' } }])], + {} + ); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ value: 42, host: 'a' }); + }); + + it('routes JsonQuery results to the JSON path', () => { + const result = buildRawTableData([makeJsonPanelData({ city: 'Paris' })], {}); + expect(result).toEqual([{ city: 'Paris' }]); + }); + + it('concatenates time-series and JSON rows when both kinds are present', () => { + const result = buildRawTableData( + [ + makeTsPanelData([{ name: 's', values: [[1000, 7]], labels: { src: 'ts' } }]), + makeJsonPanelData({ src: 'json' }), + ], + {} + ); + expect(result).toHaveLength(2); + expect(result.some((r) => r['src'] === 'ts')).toBe(true); + expect(result.some((r) => r['src'] === 'json')).toBe(true); + }); + + it('includes timestamp in instant mode (default)', () => { + const result = buildRawTableData( + [makeTsPanelData([{ name: 's', values: [[9999, 1]], labels: {} }])], + {} + ); + expect(result[0]).toHaveProperty('timestamp', 9999); + }); + + it('omits timestamp in range mode (column with plugin)', () => { + const spec: TableOptions = { + columnSettings: [{ name: 'value', plugin: { kind: 'StatChart', spec: {} } }], + }; + const result = buildRawTableData( + [makeTsPanelData([{ name: 's', values: [[9999, 1]], labels: {} }])], + spec + ); + expect(result[0]).not.toHaveProperty('timestamp'); + }); + + it('uses raw scalar value for a plugin column when forExport is true', () => { + const spec: TableOptions = { + columnSettings: [{ name: 'value', plugin: { kind: 'StatChart', spec: {} } }], + }; + const result = buildRawTableData( + [makeTsPanelData([{ name: 's', values: [[0, 3.14]], labels: {} }])], + spec, + { forExport: true } + ); + expect(result[0]?.['value']).toBe(3.14); + }); + + it('embeds PanelData object for a plugin column when not exporting', () => { + const spec: TableOptions = { + columnSettings: [{ name: 'value', plugin: { kind: 'StatChart', spec: {} } }], + }; + const result = buildRawTableData( + [makeTsPanelData([{ name: 's', values: [[0, 5]], labels: {} }])], + spec + ); + // The cell value should be an object (embedded PanelData), not a scalar. + expect(typeof result[0]?.['value']).toBe('object'); + }); + + it('returns label-only row when series has no values', () => { + const result = buildRawTableData( + [makeTsPanelData([{ name: 'empty', values: [], labels: { host: 'x' } }])], + {} + ); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ host: 'x' }); + expect(result[0]).not.toHaveProperty('value'); + expect(result[0]).not.toHaveProperty('timestamp'); + }); + + it('uses indexed column and label names for multiple time-series queries', () => { + const result = buildRawTableData( + [ + makeTsPanelData([{ name: 'q1', values: [[1000, 10]], labels: { dc: 'us' } }]), + makeTsPanelData([{ name: 'q2', values: [[1000, 20]], labels: { dc: 'eu' } }]), + ], + {} + ); + expect(result[0]).toHaveProperty('value #1', 10); + expect(result[0]).toHaveProperty('dc #1', 'us'); + expect(result[1]).toHaveProperty('value #2', 20); + expect(result[1]).toHaveProperty('dc #2', 'eu'); + expect(result[0]).not.toHaveProperty('value'); + expect(result[0]).not.toHaveProperty('dc'); + }); + + it('handles a result whose data has not loaded yet (undefined data)', () => { + const pending = { + definition: { kind: 'TimeSeriesQuery', spec: { plugin: { kind: 'P', spec: {} } } }, + } as PanelData; + expect(buildRawTableData([pending], {})).toEqual([]); + }); +}); diff --git a/table/src/table-data-utils.ts b/table/src/table-data-utils.ts index cf1f28e61..95cb6166d 100644 --- a/table/src/table-data-utils.ts +++ b/table/src/table-data-utils.ts @@ -11,9 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Labels, PanelData } from '@perses-dev/plugin-system'; -import { TimeSeries, TimeSeriesData } from '@perses-dev/spec'; -import { TableOptions } from './models'; +import { Labels, PanelData } from "@perses-dev/plugin-system"; +import { JsonData, TimeSeries, TimeSeriesData } from "@perses-dev/spec"; +import { TableOptions } from "./models"; /** * Options for building raw table data. @@ -41,12 +41,22 @@ export function getTablePanelQueryMode(spec: TableOptions): 'instant' | 'range' * and TableExportAction (for CSV export). Extracting this ensures both use the * same transformation logic, reducing drift. * - * @param queryResults - The panel query results containing time series data + * @param queryResults - The panel query results containing data to be transformed into a table * @param spec - The table options specification * @param options - Build options (e.g., forExport mode) * @returns Array of row objects with column keys and values */ export function buildRawTableData( + queryResults: Array, + spec: TableOptions, + options: BuildRawTableDataOptions = {} +): Array> { + const timeSeriesResults:Array> = queryResults.filter((r) => r.definition.kind === 'TimeSeriesQuery') + const jsonResults: Array = queryResults.filter((r) => r.definition.kind === 'JsonQuery') + return [...buildTimeSeriesTableData(timeSeriesResults, spec, options), ...buildJsonTableData(jsonResults)]; +} + +function buildTimeSeriesTableData( queryResults: Array>, spec: TableOptions, options: BuildRawTableDataOptions = {} @@ -95,3 +105,33 @@ export function buildRawTableData( } }); } + +function safeStringify(v: object): string { + try { + return JSON.stringify(v); + } catch { + return String(v); + } +} + +function toItemArray(data: unknown): unknown[] { + if (Array.isArray(data)) return data; + if (data != null) return [data]; + return []; +} + +export function buildJsonTableData(queryResults: Array>): Array> { + return queryResults.flatMap((r:PanelData) => { + return toItemArray(r.data).map(({data}: JsonData) => { + if (typeof data !== 'object' || data === null) { + return { value: data }; + } + const row: Record = {}; + for (const [k, v] of Object.entries(data as Record)) { + row[k] = typeof v === 'object' && v !== null ? safeStringify(v) : v; + } + return row; + }); + }); +} +