chore: bump @grafana/create-plugin configuration to 6.7.1 (#2167)
Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "5.26.4"
|
"version": "6.7.1",
|
||||||
|
"features": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
/*
|
|
||||||
* ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
|
|
||||||
*
|
|
||||||
* In order to extend the configuration follow the steps in
|
|
||||||
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-eslint-config
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
"extends": ["@grafana/eslint-config"],
|
|
||||||
"root": true,
|
|
||||||
"rules": {
|
|
||||||
"react/prop-types": "off"
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["src/**/*.{ts,tsx}"],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-deprecated": "warn"
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["./tests/**/*"],
|
|
||||||
"rules": {
|
|
||||||
"react-hooks/rules-of-hooks": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
43
.config/bundler/externals.ts
Normal file
43
.config/bundler/externals.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Configuration, ExternalItemFunctionData } from 'webpack';
|
||||||
|
|
||||||
|
type ExternalsType = Configuration['externals'];
|
||||||
|
|
||||||
|
export const externals: ExternalsType = [
|
||||||
|
// Required for dynamic publicPath resolution
|
||||||
|
{ 'amd-module': 'module' },
|
||||||
|
'lodash',
|
||||||
|
'jquery',
|
||||||
|
'moment',
|
||||||
|
'slate',
|
||||||
|
'emotion',
|
||||||
|
'@emotion/react',
|
||||||
|
'@emotion/css',
|
||||||
|
'prismjs',
|
||||||
|
'slate-plain-serializer',
|
||||||
|
'@grafana/slate-react',
|
||||||
|
'react',
|
||||||
|
'react-dom',
|
||||||
|
'react-redux',
|
||||||
|
'redux',
|
||||||
|
'rxjs',
|
||||||
|
'i18next',
|
||||||
|
'react-router',
|
||||||
|
'd3',
|
||||||
|
'angular',
|
||||||
|
/^@grafana\/ui/i,
|
||||||
|
/^@grafana\/runtime/i,
|
||||||
|
/^@grafana\/data/i,
|
||||||
|
|
||||||
|
// Mark legacy SDK imports as external if their name starts with the "grafana/" prefix
|
||||||
|
({ request }: ExternalItemFunctionData, callback: (error?: Error, result?: string) => void) => {
|
||||||
|
const prefix = 'grafana/';
|
||||||
|
const hasPrefix = (request: string) => request.indexOf(prefix) === 0;
|
||||||
|
const stripPrefix = (request: string) => request.slice(prefix.length);
|
||||||
|
|
||||||
|
if (request && hasPrefix(request)) {
|
||||||
|
return callback(undefined, stripPrefix(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -7,7 +7,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise}
|
grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise}
|
||||||
grafana_version: ${GRAFANA_VERSION:-12.1.1}
|
grafana_version: ${GRAFANA_VERSION:-12.2.0}
|
||||||
development: ${DEVELOPMENT:-false}
|
development: ${DEVELOPMENT:-false}
|
||||||
anonymous_auth_enabled: ${ANONYMOUS_AUTH_ENABLED:-true}
|
anonymous_auth_enabled: ${ANONYMOUS_AUTH_ENABLED:-true}
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
31
.config/eslint.config.mjs
Normal file
31
.config/eslint.config.mjs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
|
import grafanaConfig from '@grafana/eslint-config/flat.js';
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
...grafanaConfig,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/**/*.{ts,tsx}'],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-deprecated': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['./tests/**/*'],
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'react-hooks/rules-of-hooks': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
1
.config/types/setupTests.d.ts
vendored
Normal file
1
.config/types/setupTests.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -19,6 +19,7 @@ import VirtualModulesPlugin from 'webpack-virtual-modules';
|
|||||||
import { BuildModeWebpackPlugin } from './BuildModeWebpackPlugin.ts';
|
import { BuildModeWebpackPlugin } from './BuildModeWebpackPlugin.ts';
|
||||||
import { DIST_DIR, SOURCE_DIR } from './constants.ts';
|
import { DIST_DIR, SOURCE_DIR } from './constants.ts';
|
||||||
import { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, hasReadme, isWSL } from './utils.ts';
|
import { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, hasReadme, isWSL } from './utils.ts';
|
||||||
|
import { externals } from '../bundler/externals.ts';
|
||||||
|
|
||||||
const pluginJson = getPluginJson();
|
const pluginJson = getPluginJson();
|
||||||
const cpVersion = getCPConfigVersion();
|
const cpVersion = getCPConfigVersion();
|
||||||
@@ -55,45 +56,7 @@ const config = async (env: Env): Promise<Configuration> => {
|
|||||||
|
|
||||||
entry: await getEntries(),
|
entry: await getEntries(),
|
||||||
|
|
||||||
externals: [
|
externals,
|
||||||
// Required for dynamic publicPath resolution
|
|
||||||
{ 'amd-module': 'module' },
|
|
||||||
'lodash',
|
|
||||||
'jquery',
|
|
||||||
'moment',
|
|
||||||
'slate',
|
|
||||||
'emotion',
|
|
||||||
'@emotion/react',
|
|
||||||
'@emotion/css',
|
|
||||||
'prismjs',
|
|
||||||
'slate-plain-serializer',
|
|
||||||
'@grafana/slate-react',
|
|
||||||
'react',
|
|
||||||
'react-dom',
|
|
||||||
'react-redux',
|
|
||||||
'redux',
|
|
||||||
'rxjs',
|
|
||||||
'i18next',
|
|
||||||
'react-router',
|
|
||||||
'd3',
|
|
||||||
'angular',
|
|
||||||
/^@grafana\/ui/i,
|
|
||||||
/^@grafana\/runtime/i,
|
|
||||||
/^@grafana\/data/i,
|
|
||||||
|
|
||||||
// Mark legacy SDK imports as external if their name starts with the "grafana/" prefix
|
|
||||||
({ request }, callback) => {
|
|
||||||
const prefix = 'grafana/';
|
|
||||||
const hasPrefix = (request: string) => request.indexOf(prefix) === 0;
|
|
||||||
const stripPrefix = (request: string) => request.substr(prefix.length);
|
|
||||||
|
|
||||||
if (request && hasPrefix(request)) {
|
|
||||||
return callback(undefined, stripPrefix(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
// Support WebAssembly according to latest spec - makes WebAssembly module async
|
// Support WebAssembly according to latest spec - makes WebAssembly module async
|
||||||
experiments: {
|
experiments: {
|
||||||
@@ -224,7 +187,8 @@ const config = async (env: Env): Promise<Configuration> => {
|
|||||||
new ReplaceInFileWebpackPlugin([
|
new ReplaceInFileWebpackPlugin([
|
||||||
{
|
{
|
||||||
dir: DIST_DIR,
|
dir: DIST_DIR,
|
||||||
files: ['plugin.json', 'README.md'],
|
test: [/(^|\/)plugin\.json$/, /(^|\/)README\.md$/],
|
||||||
|
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
search: /\%VERSION\%/g,
|
search: /\%VERSION\%/g,
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./.config/.eslintrc",
|
|
||||||
"plugins": ["prettier"],
|
|
||||||
"ignorePatterns": ["/src/test-setup/**/*"],
|
|
||||||
"rules": {
|
|
||||||
"react-hooks/exhaustive-deps": "off",
|
|
||||||
"prettier/prettier": "error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
// Prettier configuration provided by Grafana scaffolding
|
// Prettier configuration provided by Grafana scaffolding
|
||||||
...require("./.config/.prettierrc.js")
|
...require('./.config/.prettierrc.js'),
|
||||||
};
|
};
|
||||||
|
|||||||
60
eslint.config.mjs
Normal file
60
eslint.config.mjs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
|
import baseConfig from './.config/eslint.config.mjs';
|
||||||
|
import prettier from 'eslint-plugin-prettier';
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'**/*.sublime-workspace',
|
||||||
|
'**/*.sublime-project',
|
||||||
|
'**/.idea/',
|
||||||
|
'**/.vscode',
|
||||||
|
'**/*.bat',
|
||||||
|
'**/.DS_Store',
|
||||||
|
'docs/site/',
|
||||||
|
'dist/test/',
|
||||||
|
'dist/test-setup/',
|
||||||
|
'**/vendor',
|
||||||
|
'src/vendor',
|
||||||
|
'src/vendor/npm',
|
||||||
|
'**/node_modules',
|
||||||
|
'**/coverage/',
|
||||||
|
'tmp',
|
||||||
|
'**/artifacts/',
|
||||||
|
'**/work/',
|
||||||
|
'**/test-results/',
|
||||||
|
'**/playwright-report/',
|
||||||
|
'**/blob-report/',
|
||||||
|
'playwright/.cache/',
|
||||||
|
'playwright/.auth/',
|
||||||
|
'**/npm-debug.log',
|
||||||
|
'**/yarn-error.log',
|
||||||
|
'**/dist/',
|
||||||
|
'**/ci/',
|
||||||
|
'**/alexanderzobnin-zabbix-app.zip',
|
||||||
|
'**/.eslintcache',
|
||||||
|
'public/css/*.min.css',
|
||||||
|
'**/provisioning/',
|
||||||
|
'devenv/nginx/nginx.crt',
|
||||||
|
'devenv/nginx/nginx.key',
|
||||||
|
'**/.pnp.*',
|
||||||
|
'.yarn/*',
|
||||||
|
'!.yarn/patches',
|
||||||
|
'!.yarn/plugins',
|
||||||
|
'!.yarn/releases',
|
||||||
|
'!.yarn/sdks',
|
||||||
|
'!.yarn/versions',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
prettier: prettier,
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'react-hooks/exhaustive-deps': 'off',
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
145
package.json
145
package.json
@@ -16,7 +16,7 @@
|
|||||||
"build": "webpack -c ./webpack.config.ts --env production",
|
"build": "webpack -c ./webpack.config.ts --env production",
|
||||||
"dev": "webpack -w -c ./webpack.config.ts --env development",
|
"dev": "webpack -w -c ./webpack.config.ts --env development",
|
||||||
"e2e": "playwright test",
|
"e2e": "playwright test",
|
||||||
"lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .",
|
"lint": "eslint --cache .",
|
||||||
"lint:fix": "yarn run lint --fix && prettier --write --list-different .",
|
"lint:fix": "yarn run lint --fix && prettier --write --list-different .",
|
||||||
"server": "docker compose up --build",
|
"server": "docker compose up --build",
|
||||||
"server:down": "docker compose --file ./devenv/default/docker-compose.yml down",
|
"server:down": "docker compose --file ./devenv/default/docker-compose.yml down",
|
||||||
@@ -28,89 +28,84 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/css": "11.10.6",
|
"@emotion/css": "11.13.5",
|
||||||
"@grafana/data": "^12.1.0",
|
"@grafana/data": "12.3.1",
|
||||||
"@grafana/i18n": "^12.1.0",
|
"@grafana/i18n": "12.3.1",
|
||||||
"@grafana/plugin-ui": "^0.10.10",
|
"@grafana/plugin-ui": "0.12.1",
|
||||||
"@grafana/runtime": "^12.1.0",
|
"@grafana/runtime": "12.3.1",
|
||||||
"@grafana/schema": "^12.1.0",
|
"@grafana/schema": "12.3.1",
|
||||||
"@grafana/ui": "^12.1.0",
|
"@grafana/ui": "12.3.1",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-router-dom": "^6.22.0",
|
"react-use": "17.6.0",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"tslib": "2.5.3"
|
"tslib": "2.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.28.4",
|
"@babel/core": "7.28.5",
|
||||||
"@changesets/cli": "^2.29.7",
|
"@changesets/cli": "2.29.8",
|
||||||
"@grafana/e2e-selectors": "12.1.0",
|
"@grafana/e2e-selectors": "12.3.1",
|
||||||
"@grafana/eslint-config": "^8.0.0",
|
"@grafana/eslint-config": "9.0.0",
|
||||||
"@grafana/plugin-e2e": "^2.2.3",
|
"@grafana/plugin-e2e": "3.1.1",
|
||||||
"@grafana/tsconfig": "^2.0.0",
|
"@grafana/tsconfig": "2.0.1",
|
||||||
"@playwright/test": "^1.55.0",
|
"@playwright/test": "1.57.0",
|
||||||
"@stylistic/eslint-plugin-ts": "^2.9.0",
|
"@stylistic/eslint-plugin-ts": "4.4.1",
|
||||||
"@swc/core": "^1.3.90",
|
"@swc/core": "1.15.8",
|
||||||
"@swc/helpers": "^0.5.0",
|
"@swc/helpers": "0.5.18",
|
||||||
"@swc/jest": "^0.2.26",
|
"@swc/jest": "0.2.39",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"@testing-library/jest-dom": "6.1.4",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/react": "14.0.0",
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
"@types/glob": "^8.0.0",
|
"@testing-library/react": "16.3.1",
|
||||||
"@types/grafana": "github:CorpGlory/types-grafana",
|
"@types/grafana": "github:CorpGlory/types-grafana",
|
||||||
"@types/jest": "^29.5.0",
|
"@types/jest": "30.0.0",
|
||||||
"@types/lodash": "^4.14.194",
|
"@types/lodash": "4.17.21",
|
||||||
"@types/node": "^20.19.16",
|
"@types/node": "25.0.3",
|
||||||
"@types/react": "18.3.24",
|
"@types/react": "18.3.27",
|
||||||
"@types/react-router-dom": "^5.2.0",
|
"@types/react-dom": "18.3.1",
|
||||||
"@types/testing-library__jest-dom": "5.14.8",
|
"@typescript-eslint/eslint-plugin": "8.52.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
"@typescript-eslint/parser": "8.52.0",
|
||||||
"@typescript-eslint/parser": "^8.3.0",
|
"autoprefixer": "10.4.23",
|
||||||
"autoprefixer": "10.4.7",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"clean-webpack-plugin": "^0.1.19",
|
"cspell": "9.4.0",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"css-loader": "7.1.2",
|
||||||
"cspell": "6.31.3",
|
"eslint": "9.39.2",
|
||||||
"css-loader": "^6.7.3",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint": "8.57.1",
|
"eslint-plugin-jsdoc": "61.5.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-plugin-prettier": "5.5.4",
|
||||||
"eslint-plugin-deprecation": "^2.0.0",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"eslint-plugin-jsdoc": "^46.8.2",
|
"eslint-plugin-react-hooks": "7.0.1",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-webpack-plugin": "5.0.2",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"fork-ts-checker-webpack-plugin": "9.1.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-webpack-plugin": "^4.0.1",
|
|
||||||
"fork-ts-checker-webpack-plugin": "^8.0.0",
|
|
||||||
"glob": "13.0.0",
|
"glob": "13.0.0",
|
||||||
"identity-obj-proxy": "3.0.0",
|
"identity-obj-proxy": "3.0.0",
|
||||||
"imports-loader": "^5.0.0",
|
"imports-loader": "5.0.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "30.2.0",
|
||||||
"jest-environment-jsdom": "^29.5.0",
|
"jest-environment-jsdom": "30.2.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mini-css-extract-plugin": "2.6.1",
|
"mini-css-extract-plugin": "2.9.4",
|
||||||
"moment": "2.29.4",
|
"moment": "2.30.1",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.5.6",
|
||||||
"postcss-loader": "7.0.1",
|
"postcss-loader": "8.2.0",
|
||||||
"postcss-reporter": "7.0.5",
|
"postcss-reporter": "7.1.0",
|
||||||
"postcss-scss": "4.0.4",
|
"postcss-scss": "4.0.9",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "3.7.4",
|
||||||
"prop-types": "15.7.2",
|
"replace-in-file-webpack-plugin": "1.0.6",
|
||||||
"react-use": "17.4.0",
|
"sass": "1.97.2",
|
||||||
"replace-in-file-webpack-plugin": "^1.0.6",
|
"sass-loader": "16.0.6",
|
||||||
"sass": "1.63.2",
|
"semver": "7.7.3",
|
||||||
"sass-loader": "13.3.3",
|
"style-loader": "4.0.0",
|
||||||
"semver": "^7.7.2",
|
"swc-loader": "0.2.6",
|
||||||
"style-loader": "3.3.4",
|
"terser-webpack-plugin": "5.3.16",
|
||||||
"swc-loader": "^0.2.3",
|
"ts-node": "10.9.2",
|
||||||
"terser-webpack-plugin": "^5.3.14",
|
"tsconfig-paths": "4.2.0",
|
||||||
"ts-node": "^10.9.2",
|
"typescript": "5.9.3",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"webpack": "5.104.1",
|
||||||
"typescript": "5.5.4",
|
"webpack-cli": "6.0.1",
|
||||||
"webpack": "^5.94.0",
|
"webpack-livereload-plugin": "3.0.2",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-remove-empty-scripts": "1.1.1",
|
||||||
"webpack-livereload-plugin": "^3.0.2",
|
"webpack-subresource-integrity": "5.1.0",
|
||||||
"webpack-remove-empty-scripts": "^1.0.1",
|
"webpack-virtual-modules": "0.6.2"
|
||||||
"webpack-subresource-integrity": "^5.1.0",
|
|
||||||
"webpack-virtual-modules": "^0.6.2"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class ModalController extends React.Component<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
renderModal() {
|
renderModal(): React.ReactNode {
|
||||||
const { component, props } = this.state;
|
const { component, props } = this.state;
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return null;
|
return null;
|
||||||
@@ -53,7 +53,7 @@ export class ModalController extends React.Component<Props, State> {
|
|||||||
|
|
||||||
this.modalRoot.appendChild(this.modalNode);
|
this.modalRoot.appendChild(this.modalNode);
|
||||||
const modal = React.createElement(provideTheme(component), props);
|
const modal = React.createElement(provideTheme(component), props);
|
||||||
return ReactDOM.createPortal(modal, this.modalNode);
|
return ReactDOM.createPortal(modal, this.modalNode) as React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { getDataSourceSrv, config } from '@grafana/runtime';
|
import { getDataSourceSrv, config } from '@grafana/runtime';
|
||||||
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
@@ -42,8 +42,19 @@ export const ConfigEditor = (props: Props) => {
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { options, onOptionsChange } = props;
|
const { options, onOptionsChange } = props;
|
||||||
|
|
||||||
const [selectedDBDatasource, setSelectedDBDatasource] = useState(null);
|
// Derive selectedDBDatasource and currentDSType from options
|
||||||
const [currentDSType, setCurrentDSType] = useState('');
|
const { selectedDBDatasource, currentDSType } = useMemo(() => {
|
||||||
|
if (!options.jsonData.dbConnectionEnable || !options.jsonData.dbConnectionDatasourceId) {
|
||||||
|
return { selectedDBDatasource: null, currentDSType: '' };
|
||||||
|
}
|
||||||
|
const selectedDs = getDirectDBDatasources().find(
|
||||||
|
(dsOption) => dsOption.id === options.jsonData.dbConnectionDatasourceId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
selectedDBDatasource: selectedDs ? { label: selectedDs.name, value: selectedDs.id } : null,
|
||||||
|
currentDSType: selectedDs?.type || '',
|
||||||
|
};
|
||||||
|
}, [options.jsonData.dbConnectionEnable, options.jsonData.dbConnectionDatasourceId]);
|
||||||
|
|
||||||
// Apply some defaults on initial render
|
// Apply some defaults on initial render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,16 +84,13 @@ export const ConfigEditor = (props: Props) => {
|
|||||||
secureJsonData: { ...newSecureJsonData },
|
secureJsonData: { ...newSecureJsonData },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.jsonData.dbConnectionEnable) {
|
// Handle async lookup when dbConnectionDatasourceId is not set but name is available
|
||||||
if (!options.jsonData.dbConnectionDatasourceId) {
|
if (options.jsonData.dbConnectionEnable && !options.jsonData.dbConnectionDatasourceId) {
|
||||||
const dsName = options.jsonData.dbConnectionDatasourceName;
|
const dsName = options.jsonData.dbConnectionDatasourceName;
|
||||||
getDataSourceSrv()
|
getDataSourceSrv()
|
||||||
.get(dsName)
|
.get(dsName)
|
||||||
.then((ds) => {
|
.then((ds) => {
|
||||||
if (ds) {
|
if (ds) {
|
||||||
const selectedDs = getDirectDBDatasources().find((dsOption) => dsOption.id === ds.id);
|
|
||||||
setSelectedDBDatasource({ label: selectedDs?.name, value: selectedDs?.id });
|
|
||||||
setCurrentDSType(selectedDs?.type);
|
|
||||||
onOptionsChange({
|
onOptionsChange({
|
||||||
...options,
|
...options,
|
||||||
jsonData: {
|
jsonData: {
|
||||||
@@ -92,13 +100,6 @@ export const ConfigEditor = (props: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
const selectedDs = getDirectDBDatasources().find(
|
|
||||||
(dsOption) => dsOption.id === options.jsonData.dbConnectionDatasourceId
|
|
||||||
);
|
|
||||||
setSelectedDBDatasource({ label: selectedDs?.name, value: selectedDs?.id });
|
|
||||||
setCurrentDSType(selectedDs?.type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -318,12 +319,7 @@ export const ConfigEditor = (props: Props) => {
|
|||||||
width={40}
|
width={40}
|
||||||
value={selectedDBDatasource}
|
value={selectedDBDatasource}
|
||||||
options={getDirectDBDSOptions()}
|
options={getDirectDBDSOptions()}
|
||||||
onChange={directDBDatasourceChanegeHandler(
|
onChange={directDBDatasourceChanegeHandler(options, onOptionsChange)}
|
||||||
options,
|
|
||||||
onOptionsChange,
|
|
||||||
setSelectedDBDatasource,
|
|
||||||
setCurrentDSType
|
|
||||||
)}
|
|
||||||
placeholder="Select a DB datasource (MySQL, PostgreSQL, InfluxDB)"
|
placeholder="Select a DB datasource (MySQL, PostgreSQL, InfluxDB)"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@@ -480,16 +476,8 @@ const resetSecureJsonField =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const directDBDatasourceChanegeHandler =
|
const directDBDatasourceChanegeHandler =
|
||||||
(
|
(options: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>, onChange: Props['onOptionsChange']) =>
|
||||||
options: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
|
|
||||||
onChange: Props['onOptionsChange'],
|
|
||||||
setSelectedDS: React.Dispatch<any>,
|
|
||||||
setSelectedDSType: React.Dispatch<any>
|
|
||||||
) =>
|
|
||||||
(value: SelectableValue<number>) => {
|
(value: SelectableValue<number>) => {
|
||||||
const selectedDs = getDirectDBDatasources().find((dsOption) => dsOption.id === value.value);
|
|
||||||
setSelectedDS({ label: selectedDs.name, value: selectedDs.id });
|
|
||||||
setSelectedDSType(selectedDs.type);
|
|
||||||
onChange({
|
onChange({
|
||||||
...options,
|
...options,
|
||||||
jsonData: {
|
jsonData: {
|
||||||
|
|||||||
@@ -111,8 +111,11 @@ function getProblemsQueryDefaults(): Partial<ZabbixMetricsQuery> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ZabbixQueryEditorProps
|
export interface ZabbixQueryEditorProps extends QueryEditorProps<
|
||||||
extends QueryEditorProps<ZabbixDatasource, ZabbixMetricsQuery, ZabbixDSOptions> {}
|
ZabbixDatasource,
|
||||||
|
ZabbixMetricsQuery,
|
||||||
|
ZabbixDSOptions
|
||||||
|
> {}
|
||||||
|
|
||||||
export const QueryEditor = ({ query, datasource, onChange, onRunQuery, range }: ZabbixQueryEditorProps) => {
|
export const QueryEditor = ({ query, datasource, onChange, onRunQuery, range }: ZabbixQueryEditorProps) => {
|
||||||
const [itemCount, setItemCount] = useState(0);
|
const [itemCount, setItemCount] = useState(0);
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ export interface Props {
|
|||||||
|
|
||||||
export const QueryFunctionsEditor = ({ query, onChange }: Props) => {
|
export const QueryFunctionsEditor = ({ query, onChange }: Props) => {
|
||||||
const onFuncParamChange = (func: MetricFunc, index: number, value: string) => {
|
const onFuncParamChange = (func: MetricFunc, index: number, value: string) => {
|
||||||
func.params[index] = value;
|
const functions = query.functions.map((f) => {
|
||||||
const funcIndex = query.functions.findIndex((f) => f === func);
|
if (f === func) {
|
||||||
const functions = query.functions;
|
const newParams = [...f.params];
|
||||||
functions[funcIndex] = func;
|
newParams[index] = value;
|
||||||
|
return { ...f, params: newParams };
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
});
|
||||||
onChange({ ...query, functions });
|
onChange({ ...query, functions });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { ScopedVars } from '@grafana/data';
|
import { ScopedVars } from '@grafana/data';
|
||||||
import { ZabbixDatasource } from '../datasource';
|
import { ZabbixDatasource } from '../datasource';
|
||||||
import { ZabbixMetricsQuery } from '../types/query';
|
import { ZabbixMetricsQuery } from '../types/query';
|
||||||
@@ -10,12 +10,10 @@ export const useInterpolatedQuery = (
|
|||||||
query: ZabbixMetricsQuery,
|
query: ZabbixMetricsQuery,
|
||||||
scopedVars?: ScopedVars
|
scopedVars?: ScopedVars
|
||||||
): ZabbixMetricsQuery => {
|
): ZabbixMetricsQuery => {
|
||||||
const [interpolatedQuery, setInterpolatedQuery] = useState<ZabbixMetricsQuery>(query);
|
const resolvedScopedVars = scopedVars ?? EMPTY_SCOPED_VARS;
|
||||||
const resolvedScopedVars = useMemo(() => scopedVars ?? EMPTY_SCOPED_VARS, [scopedVars]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const interpolatedQuery = useMemo(() => {
|
||||||
const replacedQuery = datasource.interpolateVariablesInQueries([query], resolvedScopedVars)[0];
|
return datasource.interpolateVariablesInQueries([query], resolvedScopedVars)[0];
|
||||||
setInterpolatedQuery(replacedQuery);
|
|
||||||
}, [datasource, query, resolvedScopedVars]);
|
}, [datasource, query, resolvedScopedVars]);
|
||||||
|
|
||||||
return interpolatedQuery;
|
return interpolatedQuery;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ jest.mock(
|
|||||||
// Provide a custom query implementation that resolves backend + frontend + db + annotations
|
// Provide a custom query implementation that resolves backend + frontend + db + annotations
|
||||||
// so tests relying on merged results receive expected data.
|
// so tests relying on merged results receive expected data.
|
||||||
if (actual && actual.DataSourceWithBackend && actual.DataSourceWithBackend.prototype) {
|
if (actual && actual.DataSourceWithBackend && actual.DataSourceWithBackend.prototype) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
actual.DataSourceWithBackend.prototype.query = function (request: any) {
|
actual.DataSourceWithBackend.prototype.query = function (request: any) {
|
||||||
const that: any = this;
|
const that: any = this;
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ export const ProblemsPanel = (props: ProblemsPanelProps) => {
|
|||||||
for (const dataFrame of data.series) {
|
for (const dataFrame of data.series) {
|
||||||
try {
|
try {
|
||||||
const values = dataFrame.fields[0].values;
|
const values = dataFrame.fields[0].values;
|
||||||
if (values.toArray) {
|
problems.push(...values);
|
||||||
problems.push(...values.toArray());
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return [];
|
return [];
|
||||||
@@ -125,7 +123,8 @@ export const ProblemsPanel = (props: ProblemsPanelProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addTagFilter = (tag: ZBXTag, datasource: DataSourceRef) => {
|
const addTagFilter = (tag: ZBXTag, datasource: DataSourceRef) => {
|
||||||
const targets = data.request?.targets!;
|
const originalTargets = data.request?.targets!;
|
||||||
|
const targets = _.cloneDeep(originalTargets);
|
||||||
let updated = false;
|
let updated = false;
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
if (target.datasource?.uid === datasource?.uid || target.datasource === datasource) {
|
if (target.datasource?.uid === datasource?.uid || target.datasource === datasource) {
|
||||||
@@ -148,7 +147,8 @@ export const ProblemsPanel = (props: ProblemsPanelProps) => {
|
|||||||
|
|
||||||
const removeTagFilter = (tag: ZBXTag, datasource: DataSourceRef) => {
|
const removeTagFilter = (tag: ZBXTag, datasource: DataSourceRef) => {
|
||||||
const matchTag = (t: ZBXTag) => t.tag === tag.tag && t.value === tag.value;
|
const matchTag = (t: ZBXTag) => t.tag === tag.tag && t.value === tag.value;
|
||||||
const targets = data.request?.targets!;
|
const originalTargets = data.request?.targets!;
|
||||||
|
const targets = _.cloneDeep(originalTargets);
|
||||||
let updated = false;
|
let updated = false;
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
if (target.datasource?.uid === datasource?.uid || target.datasource === datasource) {
|
if (target.datasource?.uid === datasource?.uid || target.datasource === datasource) {
|
||||||
@@ -170,8 +170,8 @@ export const ProblemsPanel = (props: ProblemsPanelProps) => {
|
|||||||
|
|
||||||
const getProblemEvents = async (problem: ProblemDTO) => {
|
const getProblemEvents = async (problem: ProblemDTO) => {
|
||||||
const triggerids = [problem.triggerid];
|
const triggerids = [problem.triggerid];
|
||||||
const timeFrom = Math.ceil(dateMath.parse(timeRange.from)!.unix());
|
const timeFrom = Math.ceil(dateMath.toDateTime(timeRange.from, {}).unix());
|
||||||
const timeTo = Math.ceil(dateMath.parse(timeRange.to)!.unix());
|
const timeTo = Math.ceil(dateMath.toDateTime(timeRange.to, {}).unix());
|
||||||
const ds: any = await getDataSourceSrv().get(problem.datasource);
|
const ds: any = await getDataSourceSrv().get(problem.datasource);
|
||||||
return ds.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
|
return ds.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
|
ariaLabel="Acknowledge problem"
|
||||||
onDismiss={this.dismiss}
|
onDismiss={this.dismiss}
|
||||||
className={styles.modal}
|
className={styles.modal}
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
|
|||||||
isOpen={true}
|
isOpen={true}
|
||||||
onDismiss={this.dismiss}
|
onDismiss={this.dismiss}
|
||||||
className={styles.modal}
|
className={styles.modal}
|
||||||
|
ariaLabel="Execute script"
|
||||||
title={
|
title={
|
||||||
<div className={styles.modalHeaderTitle}>
|
<div className={styles.modalHeaderTitle}>
|
||||||
{this.state.loading ? <Spinner size={18} /> : <FAIcon icon="terminal" />}
|
{this.state.loading ? <Spinner size={18} /> : <FAIcon icon="terminal" />}
|
||||||
|
|||||||
@@ -53,26 +53,22 @@ export const ProblemDetails = ({
|
|||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const problem = original;
|
||||||
if (showTimeline) {
|
if (showTimeline) {
|
||||||
fetchProblemEvents();
|
const eventsData = await getProblemEvents(problem);
|
||||||
|
setEvents(eventsData);
|
||||||
}
|
}
|
||||||
fetchProblemAlerts();
|
const alertsData = await getProblemAlerts(problem);
|
||||||
|
setAletrs(alertsData);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setShow(true);
|
setShow(true);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [original, showTimeline, getProblemEvents, getProblemAlerts]);
|
||||||
|
|
||||||
const fetchProblemEvents = async () => {
|
|
||||||
const problem = original;
|
|
||||||
const events = await getProblemEvents(problem);
|
|
||||||
setEvents(events);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchProblemAlerts = async () => {
|
|
||||||
const problem = original;
|
|
||||||
const alerts = await getProblemAlerts(problem);
|
|
||||||
setAletrs(alerts);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTagClick = (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => {
|
const handleTagClick = (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => {
|
||||||
if (onTagClick) {
|
if (onTagClick) {
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
var units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
|
let units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
|
||||||
|
|
||||||
export function parse(text, roundUp) {
|
export function parse(text, roundUp) {
|
||||||
if (!text) { return undefined; }
|
if (!text) {
|
||||||
if (moment.isMoment(text)) { return text; }
|
return undefined;
|
||||||
if (_.isDate(text)) { return moment(text); }
|
}
|
||||||
|
if (moment.isMoment(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
if (_.isDate(text)) {
|
||||||
|
return moment(text);
|
||||||
|
}
|
||||||
|
|
||||||
var time;
|
let time;
|
||||||
var mathString = '';
|
let mathString = '';
|
||||||
var index;
|
let index;
|
||||||
var parseString;
|
let parseString;
|
||||||
|
|
||||||
if (text.substring(0, 3) === 'now') {
|
if (text.substring(0, 3) === 'now') {
|
||||||
time = moment();
|
time = moment();
|
||||||
@@ -37,7 +44,7 @@ export function parse(text, roundUp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isValid(text) {
|
export function isValid(text) {
|
||||||
var date = parse(text);
|
let date = parse(text);
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -50,15 +57,15 @@ export function isValid(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseDateMath(mathString, time, roundUp) {
|
export function parseDateMath(mathString, time, roundUp) {
|
||||||
var dateTime = time;
|
let dateTime = time;
|
||||||
var i = 0;
|
let i = 0;
|
||||||
var len = mathString.length;
|
let len = mathString.length;
|
||||||
|
|
||||||
while (i < len) {
|
while (i < len) {
|
||||||
var c = mathString.charAt(i++);
|
let c = mathString.charAt(i++);
|
||||||
var type;
|
let type;
|
||||||
var num;
|
let num;
|
||||||
var unit;
|
let unit;
|
||||||
|
|
||||||
if (c === '/') {
|
if (c === '/') {
|
||||||
type = 0;
|
type = 0;
|
||||||
@@ -75,10 +82,12 @@ export function parseDateMath(mathString, time, roundUp) {
|
|||||||
} else if (mathString.length === 2) {
|
} else if (mathString.length === 2) {
|
||||||
num = mathString.charAt(i);
|
num = mathString.charAt(i);
|
||||||
} else {
|
} else {
|
||||||
var numFrom = i;
|
let numFrom = i;
|
||||||
while (!isNaN(mathString.charAt(i))) {
|
while (!isNaN(mathString.charAt(i))) {
|
||||||
i++;
|
i++;
|
||||||
if (i > 10) { return undefined; }
|
if (i > 10) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
num = parseInt(mathString.substring(numFrom, i), 10);
|
num = parseInt(mathString.substring(numFrom, i), 10);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,24 +26,19 @@ export class PanelCtrl {
|
|||||||
this.timing = {};
|
this.timing = {};
|
||||||
this.events = {
|
this.events = {
|
||||||
on: () => {},
|
on: () => {},
|
||||||
emit: () => {}
|
emit: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {}
|
||||||
}
|
|
||||||
|
|
||||||
renderingCompleted() {
|
renderingCompleted() {}
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
refresh() {}
|
||||||
}
|
|
||||||
|
|
||||||
publishAppEvent(evtName, evt) {
|
publishAppEvent(evtName, evt) {}
|
||||||
}
|
|
||||||
|
|
||||||
changeView(fullscreen, edit) {
|
changeView(fullscreen, edit) {}
|
||||||
}
|
|
||||||
|
|
||||||
viewPanel() {
|
viewPanel() {
|
||||||
this.changeView(true, false);
|
this.changeView(true, false);
|
||||||
@@ -57,14 +52,11 @@ export class PanelCtrl {
|
|||||||
this.changeView(false, false);
|
this.changeView(false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
initEditMode() {
|
initEditMode() {}
|
||||||
}
|
|
||||||
|
|
||||||
changeTab(newIndex) {
|
changeTab(newIndex) {}
|
||||||
}
|
|
||||||
|
|
||||||
addEditorTab(title, directiveFn, index) {
|
addEditorTab(title, directiveFn, index) {}
|
||||||
}
|
|
||||||
|
|
||||||
getMenu() {
|
getMenu() {
|
||||||
return [];
|
return [];
|
||||||
@@ -78,41 +70,29 @@ export class PanelCtrl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatePanelHeight() {
|
calculatePanelHeight() {}
|
||||||
}
|
|
||||||
|
|
||||||
render(payload) {
|
render(payload) {}
|
||||||
}
|
|
||||||
|
|
||||||
toggleEditorHelp(index) {
|
toggleEditorHelp(index) {}
|
||||||
}
|
|
||||||
|
|
||||||
duplicate() {
|
duplicate() {}
|
||||||
}
|
|
||||||
|
|
||||||
updateColumnSpan(span) {
|
updateColumnSpan(span) {}
|
||||||
}
|
|
||||||
|
|
||||||
removePanel() {
|
removePanel() {}
|
||||||
}
|
|
||||||
|
|
||||||
editPanelJson() {
|
editPanelJson() {}
|
||||||
}
|
|
||||||
|
|
||||||
replacePanel(newPanel, oldPanel) {
|
replacePanel(newPanel, oldPanel) {}
|
||||||
}
|
|
||||||
|
|
||||||
sharePanel() {
|
sharePanel() {}
|
||||||
}
|
|
||||||
|
|
||||||
getInfoMode() {
|
getInfoMode() {}
|
||||||
}
|
|
||||||
|
|
||||||
getInfoContent(options) {
|
getInfoContent(options) {}
|
||||||
}
|
|
||||||
|
|
||||||
openInspector() {
|
openInspector() {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MetricsPanelCtrl extends PanelCtrl {
|
export class MetricsPanelCtrl extends PanelCtrl {
|
||||||
|
|||||||
Reference in New Issue
Block a user