chore: bump @grafana/create-plugin configuration to 5.26.4 (#2082)
Bumps [`@grafana/create-plugin`](https://github.com/grafana/plugin-tools/tree/main/packages/create-plugin) configuration from 4.2.1 to 5.26.4. **Notes for reviewer:** This is an auto-generated PR which ran `@grafana/create-plugin update`. Please consult the create-plugin [CHANGELOG.md](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/CHANGELOG.md) to understand what may have changed. Please review the changes thoroughly before merging. --------- Co-authored-by: grafana-plugins-platform-bot[bot] <144369747+grafana-plugins-platform-bot[bot]@users.noreply.github.com> Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
committed by
GitHub
parent
e76741b453
commit
b13d567eee
5
.changeset/fine-snails-move.md
Normal file
5
.changeset/fine-snails-move.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'grafana-zabbix': major
|
||||
---
|
||||
|
||||
Bump grafana dependency to 11.6.0
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "4.2.1"
|
||||
"version": "5.26.4"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* ⚠️⚠️⚠️ 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/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-eslint-config
|
||||
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-eslint-config
|
||||
*/
|
||||
{
|
||||
"extends": ["@grafana/eslint-config"],
|
||||
@@ -12,14 +12,19 @@
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"plugins": ["deprecation"],
|
||||
"files": ["src/**/*.{ts,tsx}"],
|
||||
"rules": {
|
||||
"deprecation/deprecation": "warn"
|
||||
"@typescript-eslint/no-deprecated": "warn"
|
||||
},
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["./tests/**/*"],
|
||||
"rules": {
|
||||
"react-hooks/rules-of-hooks": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,14 +3,52 @@ ARG grafana_image=grafana-enterprise
|
||||
|
||||
FROM grafana/${grafana_image}:${grafana_version}
|
||||
|
||||
ARG anonymous_auth_enabled=true
|
||||
ARG development=false
|
||||
ARG TARGETARCH
|
||||
|
||||
|
||||
ENV DEV "${development}"
|
||||
|
||||
# Make it as simple as possible to access the grafana instance for development purposes
|
||||
# Do NOT enable these settings in a public facing / production grafana instance
|
||||
ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin"
|
||||
ENV GF_AUTH_ANONYMOUS_ENABLED "true"
|
||||
ENV GF_AUTH_ANONYMOUS_ENABLED "${anonymous_auth_enabled}"
|
||||
ENV GF_AUTH_BASIC_ENABLED "false"
|
||||
# Set development mode so plugins can be loaded without the need to sign
|
||||
ENV GF_DEFAULT_APP_MODE "development"
|
||||
|
||||
# Inject livereload script into grafana index.html
|
||||
|
||||
LABEL maintainer="Grafana Labs <hello@grafana.com>"
|
||||
|
||||
ENV GF_PATHS_HOME="/usr/share/grafana"
|
||||
WORKDIR $GF_PATHS_HOME
|
||||
|
||||
USER root
|
||||
|
||||
# Installing supervisor and inotify-tools
|
||||
RUN if [ "${development}" = "true" ]; then \
|
||||
if grep -i -q alpine /etc/issue; then \
|
||||
apk add supervisor inotify-tools git; \
|
||||
elif grep -i -q ubuntu /etc/issue; then \
|
||||
DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y supervisor inotify-tools git && \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
else \
|
||||
echo 'ERROR: Unsupported base image' && /bin/false; \
|
||||
fi \
|
||||
fi
|
||||
|
||||
COPY supervisord/supervisord.conf /etc/supervisor.d/supervisord.ini
|
||||
COPY supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
|
||||
|
||||
# Inject livereload script into grafana index.html
|
||||
RUN sed -i 's|</body>|<script src="http://localhost:35729/livereload.js"></script></body>|g' /usr/share/grafana/public/views/index.html
|
||||
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@@ -106,9 +106,9 @@ We are going to use [`webpack-merge`](https://github.com/survivejs/webpack-merge
|
||||
// webpack.config.ts
|
||||
import type { Configuration } from 'webpack';
|
||||
import { merge } from 'webpack-merge';
|
||||
import grafanaConfig from './.config/webpack/webpack.config';
|
||||
import grafanaConfig, { type Env } from './.config/webpack/webpack.config';
|
||||
|
||||
const config = async (env): Promise<Configuration> => {
|
||||
const config = async (env: Env): Promise<Configuration> => {
|
||||
const baseConfig = await grafanaConfig(env);
|
||||
|
||||
return merge(baseConfig, {
|
||||
@@ -151,14 +151,15 @@ version: '3.7'
|
||||
|
||||
services:
|
||||
grafana:
|
||||
container_name: 'myorg-basic-app'
|
||||
extends:
|
||||
file: .config/docker-compose-base.yaml
|
||||
service: grafana
|
||||
build:
|
||||
context: ./.config
|
||||
args:
|
||||
grafana_version: ${GRAFANA_VERSION:-9.1.2}
|
||||
grafana_image: ${GRAFANA_IMAGE:-grafana}
|
||||
```
|
||||
|
||||
In this example, we assign the environment variable `GRAFANA_IMAGE` to the build arg `grafana_image` with a default value of `grafana`. This will allow you to set the value while running the docker-compose commands, which might be convenient in some scenarios.
|
||||
In this example, we assign the environment variable `GRAFANA_IMAGE` to the build arg `grafana_image` with a default value of `grafana`. This will allow you to set the value while running the docker compose commands, which might be convenient in some scenarios.
|
||||
|
||||
---
|
||||
|
||||
25
.config/docker-compose-base.yaml
Normal file
25
.config/docker-compose-base.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
grafana:
|
||||
user: root
|
||||
container_name: 'alexanderzobnin-zabbix-app'
|
||||
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise}
|
||||
grafana_version: ${GRAFANA_VERSION:-12.1.1}
|
||||
development: ${DEVELOPMENT:-false}
|
||||
anonymous_auth_enabled: ${ANONYMOUS_AUTH_ENABLED:-true}
|
||||
ports:
|
||||
- 3000:3000/tcp
|
||||
volumes:
|
||||
- ../dist:/var/lib/grafana/plugins/alexanderzobnin-zabbix-app
|
||||
- ../provisioning:/etc/grafana/provisioning
|
||||
- ..:/root/alexanderzobnin-zabbix-app
|
||||
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
GF_LOG_FILTERS: plugin.alexanderzobnin-zabbix-app:debug
|
||||
GF_LOG_LEVEL: debug
|
||||
GF_DATAPROXY_LOGGING: 1
|
||||
GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: alexanderzobnin-zabbix-app
|
||||
18
.config/entrypoint.sh
Normal file
18
.config/entrypoint.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "${DEV}" = "false" ]; then
|
||||
echo "Starting test mode"
|
||||
exec /run.sh
|
||||
fi
|
||||
|
||||
echo "Starting development mode"
|
||||
|
||||
if grep -i -q alpine /etc/issue; then
|
||||
exec /usr/bin/supervisord -c /etc/supervisord.conf
|
||||
elif grep -i -q ubuntu /etc/issue; then
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
||||
else
|
||||
echo 'ERROR: Unsupported base image'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
* ⚠️⚠️⚠️ 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/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config
|
||||
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-jest-config
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { TextEncoder, TextDecoder } from 'util';
|
||||
|
||||
Object.assign(global, { TextDecoder, TextEncoder });
|
||||
|
||||
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
Object.defineProperty(global, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
value: (query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
@@ -19,7 +22,7 @@ Object.defineProperty(global, 'matchMedia', {
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
HTMLCanvasElement.prototype.getContext = () => {};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* ⚠️⚠️⚠️ 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/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config
|
||||
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-jest-config
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
@@ -40,4 +40,5 @@ module.exports = {
|
||||
// Jest will throw `Cannot use import statement outside module` if it tries to load an
|
||||
// ES module without it being transformed first. ./config/README.md#esm-errors-with-jest
|
||||
transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)],
|
||||
watchPathIgnorePatterns: ['<rootDir>/node_modules', '<rootDir>/dist'],
|
||||
};
|
||||
|
||||
@@ -14,12 +14,18 @@ const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNam
|
||||
const grafanaESModules = [
|
||||
'.pnpm', // Support using pnpm symlinked packages
|
||||
'@grafana/schema',
|
||||
'@wojtekmaj/date-utils',
|
||||
'd3',
|
||||
'd3-color',
|
||||
'd3-force',
|
||||
'd3-interpolate',
|
||||
'd3-scale-chromatic',
|
||||
'get-user-locale',
|
||||
'marked',
|
||||
'memoize',
|
||||
'mimic-function',
|
||||
'ol',
|
||||
'react-calendar',
|
||||
'react-colorful',
|
||||
'rxjs',
|
||||
'uuid',
|
||||
|
||||
15
.config/supervisord/supervisord.conf
Normal file
15
.config/supervisord/supervisord.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
|
||||
[program:grafana]
|
||||
user=root
|
||||
directory=/var/lib/grafana
|
||||
command=/run.sh
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
killasgroup=true
|
||||
stopasgroup=true
|
||||
autostart=true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* ⚠️⚠️⚠️ 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/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-typescript-config
|
||||
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-typescript-config
|
||||
*/
|
||||
{
|
||||
"compilerOptions": {
|
||||
|
||||
37
.config/types/bundler-rules.d.ts
vendored
Normal file
37
.config/types/bundler-rules.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
// Image declarations
|
||||
declare module '*.gif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.jpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.webp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// Font declarations
|
||||
declare module '*.woff';
|
||||
declare module '*.woff2';
|
||||
declare module '*.eot';
|
||||
declare module '*.ttf';
|
||||
declare module '*.otf';
|
||||
83
.config/types/webpack-plugins.d.ts
vendored
Normal file
83
.config/types/webpack-plugins.d.ts
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
declare module 'replace-in-file-webpack-plugin' {
|
||||
import { Compiler, Plugin } from 'webpack';
|
||||
|
||||
interface ReplaceRule {
|
||||
search: string | RegExp;
|
||||
replace: string | ((match: string) => string);
|
||||
}
|
||||
|
||||
interface ReplaceOption {
|
||||
dir?: string;
|
||||
files?: string[];
|
||||
test?: RegExp | RegExp[];
|
||||
rules: ReplaceRule[];
|
||||
}
|
||||
|
||||
class ReplaceInFilePlugin extends Plugin {
|
||||
constructor(options?: ReplaceOption[]);
|
||||
options: ReplaceOption[];
|
||||
apply(compiler: Compiler): void;
|
||||
}
|
||||
|
||||
export = ReplaceInFilePlugin;
|
||||
}
|
||||
|
||||
declare module 'webpack-livereload-plugin' {
|
||||
import { ServerOptions } from 'https';
|
||||
import { Compiler, Plugin, Stats, Compilation } from 'webpack';
|
||||
|
||||
interface Options extends Pick<ServerOptions, 'cert' | 'key' | 'pfx'> {
|
||||
/**
|
||||
* protocol for livereload `<script>` src attribute value
|
||||
* @default protocol of the page, either `http` or `https`
|
||||
*/
|
||||
protocol?: string | undefined;
|
||||
/**
|
||||
* The desired port for the livereload server.
|
||||
* If you define port 0, an available port will be searched for, starting from 35729.
|
||||
* @default 35729
|
||||
*/
|
||||
port?: number | undefined;
|
||||
/**
|
||||
* he desired hostname for the appended `<script>` (if present) to point to
|
||||
* @default hostname of the page, like `localhost` or 10.0.2.2
|
||||
*/
|
||||
hostname?: string | undefined;
|
||||
/**
|
||||
* livereload `<script>` automatically to `<head>`.
|
||||
* @default false
|
||||
*/
|
||||
appendScriptTag?: boolean | undefined;
|
||||
/**
|
||||
* RegExp of files to ignore. Null value means ignore nothing.
|
||||
* It is also possible to define an array and use multiple anymatch patterns
|
||||
*/
|
||||
ignore?: RegExp | RegExp[] | null | undefined;
|
||||
/**
|
||||
* amount of milliseconds by which to delay the live reload (in case build takes longer)
|
||||
* @default 0
|
||||
*/
|
||||
delay?: number | undefined;
|
||||
/**
|
||||
* create hash for each file source and only notify livereload if hash has changed
|
||||
* @default false
|
||||
*/
|
||||
useSourceHash?: boolean | undefined;
|
||||
}
|
||||
|
||||
class LiveReloadPlugin extends Plugin {
|
||||
readonly isRunning: boolean;
|
||||
constructor(options?: Options);
|
||||
|
||||
apply(compiler: Compiler): void;
|
||||
|
||||
start(watching: any, cb: () => void): void;
|
||||
done(stats: Stats): void;
|
||||
failed(): void;
|
||||
autoloadJs(): string;
|
||||
scriptTag(source: string): string;
|
||||
applyCompilation(compilation: Compilation): void;
|
||||
}
|
||||
|
||||
export = LiveReloadPlugin;
|
||||
}
|
||||
33
.config/webpack/BuildModeWebpackPlugin.ts
Normal file
33
.config/webpack/BuildModeWebpackPlugin.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import webpack, { type Compiler } from 'webpack';
|
||||
|
||||
const PLUGIN_NAME = 'BuildModeWebpack';
|
||||
|
||||
export class BuildModeWebpackPlugin {
|
||||
apply(compiler: webpack.Compiler) {
|
||||
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
||||
compilation.hooks.processAssets.tap(
|
||||
{
|
||||
name: PLUGIN_NAME,
|
||||
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
|
||||
},
|
||||
async () => {
|
||||
const assets = compilation.getAssets();
|
||||
for (const asset of assets) {
|
||||
if (asset.name.endsWith('plugin.json')) {
|
||||
const pluginJsonString = asset.source.source().toString();
|
||||
const pluginJsonWithBuildMode = JSON.stringify(
|
||||
{
|
||||
...JSON.parse(pluginJsonString),
|
||||
buildMode: compilation.options.mode,
|
||||
},
|
||||
null,
|
||||
4
|
||||
);
|
||||
compilation.updateAsset(asset.name, new webpack.sources.RawSource(pluginJsonWithBuildMode));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import process from 'process';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { glob } from 'glob';
|
||||
import { SOURCE_DIR } from './constants';
|
||||
import { SOURCE_DIR } from './constants.ts';
|
||||
|
||||
export function isWSL() {
|
||||
if (process.platform !== 'linux') {
|
||||
@@ -21,12 +21,22 @@ export function isWSL() {
|
||||
}
|
||||
}
|
||||
|
||||
function loadJson(path: string) {
|
||||
const rawJson = fs.readFileSync(path, 'utf8');
|
||||
return JSON.parse(rawJson);
|
||||
}
|
||||
|
||||
export function getPackageJson() {
|
||||
return require(path.resolve(process.cwd(), 'package.json'));
|
||||
return loadJson(path.resolve(process.cwd(), 'package.json'));
|
||||
}
|
||||
|
||||
export function getPluginJson() {
|
||||
return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`));
|
||||
return loadJson(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`));
|
||||
}
|
||||
|
||||
export function getCPConfigVersion() {
|
||||
const cprcJson = path.resolve(process.cwd(), './.config', '.cprc.json');
|
||||
return fs.existsSync(cprcJson) ? loadJson(cprcJson).version : { version: 'unknown' };
|
||||
}
|
||||
|
||||
export function hasReadme() {
|
||||
@@ -35,7 +45,7 @@ export function hasReadme() {
|
||||
|
||||
// Support bundling nested plugins by finding all plugin.json files in src directory
|
||||
// then checking for a sibling module.[jt]sx? file.
|
||||
export async function getEntries(): Promise<Record<string, string>> {
|
||||
export async function getEntries() {
|
||||
const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true });
|
||||
|
||||
const plugins = await Promise.all(
|
||||
@@ -45,14 +55,14 @@ export async function getEntries(): Promise<Record<string, string>> {
|
||||
})
|
||||
);
|
||||
|
||||
return plugins.reduce((result, modules) => {
|
||||
return modules.reduce((result, module) => {
|
||||
return plugins.reduce<Record<string, string>>((result, modules) => {
|
||||
return modules.reduce((innerResult, module) => {
|
||||
const pluginPath = path.dirname(module);
|
||||
const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, '');
|
||||
const entryName = pluginName === '' ? 'module' : `${pluginName}/module`;
|
||||
|
||||
result[entryName] = module;
|
||||
return result;
|
||||
innerResult[entryName] = module;
|
||||
return innerResult;
|
||||
}, result);
|
||||
}, {});
|
||||
}
|
||||
|
||||
@@ -2,28 +2,50 @@
|
||||
* ⚠️⚠️⚠️ 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/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-webpack-config
|
||||
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-webpack-config
|
||||
*/
|
||||
|
||||
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||
import ESLintPlugin from 'eslint-webpack-plugin';
|
||||
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
|
||||
import LiveReloadPlugin from 'webpack-livereload-plugin';
|
||||
import path from 'path';
|
||||
import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
|
||||
import { Configuration } from 'webpack';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';
|
||||
import webpack, { type Configuration } from 'webpack';
|
||||
import LiveReloadPlugin from 'webpack-livereload-plugin';
|
||||
import VirtualModulesPlugin from 'webpack-virtual-modules';
|
||||
|
||||
import { getPackageJson, getPluginJson, hasReadme, getEntries, isWSL } from './utils';
|
||||
import { SOURCE_DIR, DIST_DIR } from './constants';
|
||||
import { BuildModeWebpackPlugin } from './BuildModeWebpackPlugin.ts';
|
||||
import { DIST_DIR, SOURCE_DIR } from './constants.ts';
|
||||
import { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, hasReadme, isWSL } from './utils.ts';
|
||||
|
||||
const pluginJson = getPluginJson();
|
||||
const cpVersion = getCPConfigVersion();
|
||||
const pluginVersion = getPackageJson().version;
|
||||
|
||||
const config = async (env): Promise<Configuration> => {
|
||||
const virtualPublicPath = new VirtualModulesPlugin({
|
||||
'node_modules/grafana-public-path.js': `
|
||||
import amdMetaModule from 'amd-module';
|
||||
|
||||
__webpack_public_path__ =
|
||||
amdMetaModule && amdMetaModule.uri
|
||||
? amdMetaModule.uri.slice(0, amdMetaModule.uri.lastIndexOf('/') + 1)
|
||||
: 'public/plugins/${pluginJson.id}/';
|
||||
`,
|
||||
});
|
||||
|
||||
export type Env = {
|
||||
[key: string]: true | string | Env;
|
||||
};
|
||||
|
||||
const config = async (env: Env): Promise<Configuration> => {
|
||||
const baseConfig: Configuration = {
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
buildDependencies: {
|
||||
config: [__filename],
|
||||
// __filename doesn't work in Node 24
|
||||
config: [path.resolve(process.cwd(), '.config', 'webpack', 'webpack.config.ts')],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -34,6 +56,8 @@ const config = async (env): Promise<Configuration> => {
|
||||
entry: await getEntries(),
|
||||
|
||||
externals: [
|
||||
// Required for dynamic publicPath resolution
|
||||
{ 'amd-module': 'module' },
|
||||
'lodash',
|
||||
'jquery',
|
||||
'moment',
|
||||
@@ -49,21 +73,21 @@ const config = async (env): Promise<Configuration> => {
|
||||
'react-redux',
|
||||
'redux',
|
||||
'rxjs',
|
||||
'i18next',
|
||||
'react-router',
|
||||
'react-router-dom',
|
||||
'd3',
|
||||
'angular',
|
||||
'@grafana/ui',
|
||||
'@grafana/runtime',
|
||||
'@grafana/data',
|
||||
/^@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) => request.indexOf(prefix) === 0;
|
||||
const stripPrefix = (request) => request.substr(prefix.length);
|
||||
const hasPrefix = (request: string) => request.indexOf(prefix) === 0;
|
||||
const stripPrefix = (request: string) => request.substr(prefix.length);
|
||||
|
||||
if (hasPrefix(request)) {
|
||||
if (request && hasPrefix(request)) {
|
||||
return callback(undefined, stripPrefix(request));
|
||||
}
|
||||
|
||||
@@ -71,10 +95,27 @@ const config = async (env): Promise<Configuration> => {
|
||||
},
|
||||
],
|
||||
|
||||
// Support WebAssembly according to latest spec - makes WebAssembly module async
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
|
||||
mode: env.production ? 'production' : 'development',
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
// This must come first in the rules array otherwise it breaks sourcemaps.
|
||||
{
|
||||
test: /src\/(?:.*\/)?module\.tsx?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'imports-loader',
|
||||
options: {
|
||||
imports: `side-effects grafana-public-path`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
exclude: /(node_modules)/,
|
||||
test: /\.[tj]sx?$/,
|
||||
@@ -82,7 +123,7 @@ const config = async (env): Promise<Configuration> => {
|
||||
loader: 'swc-loader',
|
||||
options: {
|
||||
jsc: {
|
||||
baseUrl: path.resolve(__dirname, 'src'),
|
||||
baseUrl: path.resolve(process.cwd(), SOURCE_DIR),
|
||||
target: 'es2015',
|
||||
loose: false,
|
||||
parser: {
|
||||
@@ -107,9 +148,6 @@ const config = async (env): Promise<Configuration> => {
|
||||
test: /\.(png|jpe?g|gif|svg)$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
// Keep publicPath relative for host.com/grafana/ deployments
|
||||
publicPath: `public/plugins/${pluginJson.id}/img/`,
|
||||
outputPath: 'img/',
|
||||
filename: Boolean(env.production) ? '[hash][ext]' : '[file]',
|
||||
},
|
||||
},
|
||||
@@ -117,29 +155,53 @@ const config = async (env): Promise<Configuration> => {
|
||||
test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
// Keep publicPath relative for host.com/grafana/ deployments
|
||||
publicPath: `public/plugins/${pluginJson.id}/fonts/`,
|
||||
outputPath: 'fonts/',
|
||||
filename: Boolean(env.production) ? '[hash][ext]' : '[name][ext]',
|
||||
filename: Boolean(env.production) ? '[hash][ext]' : '[file]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
optimization: {
|
||||
minimize: Boolean(env.production),
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
format: {
|
||||
comments: (_, { type, value }) => type === 'comment2' && value.trim().startsWith('[create-plugin]'),
|
||||
},
|
||||
compress: {
|
||||
drop_console: ['log', 'info'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
output: {
|
||||
clean: {
|
||||
keep: new RegExp(`(.*?_(amd64|arm(64)?)(.exe)?|go_plugin_build_manifest)`),
|
||||
},
|
||||
filename: '[name].js',
|
||||
chunkFilename: env.production ? '[name].js?_cache=[contenthash]' : '[name].js',
|
||||
library: {
|
||||
type: 'amd',
|
||||
},
|
||||
path: path.resolve(process.cwd(), DIST_DIR),
|
||||
publicPath: `public/plugins/${pluginJson.id}/`,
|
||||
uniqueName: pluginJson.id,
|
||||
crossOriginLoading: 'anonymous',
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new BuildModeWebpackPlugin(),
|
||||
virtualPublicPath,
|
||||
// Insert create plugin version information into the bundle
|
||||
new webpack.BannerPlugin({
|
||||
banner: `/* [create-plugin] version: ${cpVersion} */
|
||||
/* [create-plugin] plugin: ${pluginJson.id}@${pluginVersion} */`,
|
||||
raw: true,
|
||||
entryOnly: true,
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
// If src/README.md exists use it; otherwise the root README
|
||||
@@ -148,14 +210,14 @@ const config = async (env): Promise<Configuration> => {
|
||||
{ from: 'plugin.json', to: '.' },
|
||||
{ from: '../LICENSE', to: '.' },
|
||||
{ from: '../CHANGELOG.md', to: '.', force: true },
|
||||
{ from: '**/*.json', to: '.' }, // TODO<Add an error for checking the basic structure of the repo>
|
||||
{ from: '**/*.svg', to: '.', noErrorOnMissing: true }, // Optional
|
||||
{ from: '**/*.png', to: '.', noErrorOnMissing: true }, // Optional
|
||||
{ from: '**/*.html', to: '.', noErrorOnMissing: true }, // Optional
|
||||
{ from: 'img/**/*', to: '.', noErrorOnMissing: true }, // Optional
|
||||
{ from: 'libs/**/*', to: '.', noErrorOnMissing: true }, // Optional
|
||||
{ from: 'static/**/*', to: '.', noErrorOnMissing: true }, // Optional
|
||||
{ from: '**/query_help.md', to: '.', noErrorOnMissing: true }, // Optional
|
||||
{ from: '**/*.json', to: '.' },
|
||||
{ from: '**/*.svg', to: '.', noErrorOnMissing: true },
|
||||
{ from: '**/*.png', to: '.', noErrorOnMissing: true },
|
||||
{ from: '**/*.html', to: '.', noErrorOnMissing: true },
|
||||
{ from: 'img/**/*', to: '.', noErrorOnMissing: true },
|
||||
{ from: 'libs/**/*', to: '.', noErrorOnMissing: true },
|
||||
{ from: 'static/**/*', to: '.', noErrorOnMissing: true },
|
||||
{ from: '**/query_help.md', to: '.', noErrorOnMissing: true },
|
||||
],
|
||||
}),
|
||||
// Replace certain template-variables in the README and plugin.json
|
||||
@@ -166,7 +228,7 @@ const config = async (env): Promise<Configuration> => {
|
||||
rules: [
|
||||
{
|
||||
search: /\%VERSION\%/g,
|
||||
replace: getPackageJson().version,
|
||||
replace: pluginVersion,
|
||||
},
|
||||
{
|
||||
search: /\%TODAY\%/g,
|
||||
@@ -179,6 +241,9 @@ const config = async (env): Promise<Configuration> => {
|
||||
],
|
||||
},
|
||||
]),
|
||||
new SubresourceIntegrityPlugin({
|
||||
hashFuncNames: ['sha256'],
|
||||
}),
|
||||
...(env.development
|
||||
? [
|
||||
new LiveReloadPlugin(),
|
||||
@@ -192,6 +257,7 @@ const config = async (env): Promise<Configuration> => {
|
||||
new ESLintPlugin({
|
||||
extensions: ['.ts', '.tsx'],
|
||||
lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files
|
||||
failOnError: Boolean(env.production),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
|
||||
7
.cprc.json
Normal file
7
.cprc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"features": {
|
||||
"bundleGrafanaUI": false,
|
||||
"useReactRouterV6": true,
|
||||
"useExperimentalRspack": false
|
||||
}
|
||||
}
|
||||
19
.github/workflows/is-compatible.yml
vendored
19
.github/workflows/is-compatible.yml
vendored
@@ -1,21 +1,28 @@
|
||||
name: Latest Grafana API compatibility check
|
||||
on: [pull_request]
|
||||
permissions: {}
|
||||
name: Compatibility check
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
compatibilitycheck:
|
||||
permissions:
|
||||
# Required permissions when comment-pr is set to 'yes': pull-requests: write, contents: read
|
||||
pull-requests: write
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable --prefer-offline
|
||||
run: yarn install
|
||||
- name: Build plugin
|
||||
run: yarn build
|
||||
- name: Compatibility check
|
||||
run: npx @grafana/levitate@latest is-compatible --path src/module.ts --target @grafana/data,@grafana/ui,@grafana/runtime
|
||||
uses: grafana/plugin-actions/is-compatible@main
|
||||
with:
|
||||
module: './src/module.ts'
|
||||
comment-pr: 'yes'
|
||||
fail-if-incompatible: 'no'
|
||||
|
||||
5
.github/workflows/publish.yaml
vendored
5
.github/workflows/publish.yaml
vendored
@@ -29,9 +29,10 @@ jobs:
|
||||
name: CD
|
||||
uses: grafana/plugin-ci-workflows/.github/workflows/cd.yml@main
|
||||
with:
|
||||
go-version: '1.24'
|
||||
golangci-lint-version: '1.64.6'
|
||||
go-version: '1.25'
|
||||
golangci-lint-version: '2.4.0'
|
||||
branch: ${{ github.event.inputs.branch }}
|
||||
environment: ${{ github.event.inputs.environment }}
|
||||
docs-only: ${{ fromJSON(github.event.inputs.docs-only) }}
|
||||
github-draft-release: false
|
||||
run-playwright: true
|
||||
|
||||
4
.github/workflows/push.yaml
vendored
4
.github/workflows/push.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
name: CI
|
||||
uses: grafana/plugin-ci-workflows/.github/workflows/ci.yml@main
|
||||
with:
|
||||
go-version: '1.24'
|
||||
golangci-lint-version: '1.64.6'
|
||||
go-version: '1.25'
|
||||
golangci-lint-version: '2.4.0'
|
||||
plugin-version-suffix: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
|
||||
run-playwright: true
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -55,3 +55,11 @@ provisioning/
|
||||
# SSL certificates
|
||||
devenv/nginx/nginx.crt
|
||||
devenv/nginx/nginx.key
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
942
.yarn/releases/yarn-4.9.4.cjs
vendored
Executable file
942
.yarn/releases/yarn-4.9.4.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
3
.yarnrc.yml
Normal file
3
.yarnrc.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.4.cjs
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
if [ "$1" == "-h" ]; then
|
||||
echo "Usage: ${BASH_SOURCE[0]} [plugin process name] [port]"
|
||||
exit
|
||||
fi
|
||||
|
||||
PORT="${2:-3222}"
|
||||
PLUGIN_NAME="${1:-gpx_zabbix-plugin_}"
|
||||
|
||||
# Build optimized for debug
|
||||
make build-debug
|
||||
|
||||
# Reload plugin
|
||||
pkill ${PLUGIN_NAME}
|
||||
sleep 2
|
||||
|
||||
if [ "$OSTYPE" == "linux-gnu" ]; then
|
||||
ptrace_scope=`cat /proc/sys/kernel/yama/ptrace_scope`
|
||||
if [ "$ptrace_scope" != 0 ]; then
|
||||
echo "WARNING: ptrace_scope set to value other than 0, this might prevent debugger from connecting, try writing \"0\" to /proc/sys/kernel/yama/ptrace_scope.
|
||||
Read more at https://www.kernel.org/doc/Documentation/security/Yama.txt"
|
||||
read -p "Set ptrace_scope to 0? y/N (default N)" set_ptrace_input
|
||||
if [ "$set_ptrace_input" == "y" ] || [ "$set_ptrace_input" == "Y" ]; then
|
||||
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
PLUGIN_PID=`pgrep ${PLUGIN_NAME}`
|
||||
dlv attach ${PLUGIN_PID} --headless --listen=:${PORT} --api-version 2 --log
|
||||
pkill dlv
|
||||
@@ -1,10 +0,0 @@
|
||||
app_mode = development
|
||||
|
||||
[log]
|
||||
level = debug
|
||||
|
||||
[plugins]
|
||||
enable_alpha = true
|
||||
|
||||
[plugin.alexanderzobnin-zabbix-datasource]
|
||||
path = /grafana-zabbix
|
||||
@@ -15,13 +15,12 @@ services:
|
||||
|
||||
# Grafana
|
||||
grafana:
|
||||
image: grafana/grafana:main
|
||||
ports:
|
||||
- '3001:3000'
|
||||
extends:
|
||||
file: ../../.config/docker-compose-base.yaml
|
||||
service: grafana
|
||||
volumes:
|
||||
- ../..:/grafana-zabbix
|
||||
- ../dashboards:/devenv/dashboards
|
||||
- ../grafana.ini:/etc/grafana/grafana.ini:ro
|
||||
- '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml'
|
||||
- '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml'
|
||||
|
||||
|
||||
@@ -15,13 +15,12 @@ services:
|
||||
|
||||
# Grafana
|
||||
grafana:
|
||||
image: grafana/grafana:main
|
||||
ports:
|
||||
- '3001:3000'
|
||||
extends:
|
||||
file: ../../.config/docker-compose-base.yaml
|
||||
service: grafana
|
||||
volumes:
|
||||
- ../..:/grafana-zabbix
|
||||
- ../dashboards:/devenv/dashboards
|
||||
- ../grafana.ini:/etc/grafana/grafana.ini:ro
|
||||
- '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml'
|
||||
- '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml'
|
||||
|
||||
|
||||
@@ -15,13 +15,12 @@ services:
|
||||
|
||||
# Grafana
|
||||
grafana:
|
||||
image: grafana/grafana:main
|
||||
ports:
|
||||
- '3001:3000'
|
||||
extends:
|
||||
file: ../../.config/docker-compose-base.yaml
|
||||
service: grafana
|
||||
volumes:
|
||||
- ../..:/grafana-zabbix
|
||||
- ../dashboards:/devenv/dashboards
|
||||
- ../grafana.ini:/etc/grafana/grafana.ini:ro
|
||||
- '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml'
|
||||
- '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml'
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
services:
|
||||
# Grafana
|
||||
grafana:
|
||||
image: grafana/grafana:main
|
||||
ports:
|
||||
- '3001:3000'
|
||||
extends:
|
||||
file: ../../.config/docker-compose-base.yaml
|
||||
service: grafana
|
||||
volumes:
|
||||
- ../..:/grafana-zabbix
|
||||
- ../dashboards:/devenv/dashboards
|
||||
- ../grafana.ini:/etc/grafana/grafana.ini:ro
|
||||
- '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml'
|
||||
- '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml'
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
services:
|
||||
# Grafana
|
||||
grafana:
|
||||
image: grafana/grafana:main
|
||||
ports:
|
||||
- '3001:3000'
|
||||
extends:
|
||||
file: ../../.config/docker-compose-base.yaml
|
||||
service: grafana
|
||||
volumes:
|
||||
- ../..:/grafana-zabbix
|
||||
- ../dashboards:/devenv/dashboards
|
||||
- ../grafana.ini:/etc/grafana/grafana.ini:ro
|
||||
- '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml'
|
||||
- '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml'
|
||||
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
version: '3.0'
|
||||
|
||||
services:
|
||||
grafana:
|
||||
container_name: 'grafana-zabbix'
|
||||
image: grafana/grafana-enterprise:${GF_VERSION:-main}
|
||||
ports:
|
||||
- 3000:3000/tcp
|
||||
volumes:
|
||||
- ./dist:/var/lib/grafana/plugins/grafana-zabbix
|
||||
- ./provisioning:/etc/grafana/provisioning
|
||||
environment:
|
||||
- TERM=linux
|
||||
- GF_DEFAULT_APP_MODE=development
|
||||
- GF_AUTH_ANONYMOUS_ENABLED=true
|
||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
|
||||
- GF_ENTERPRISE_LICENSE_TEXT=$GF_ENTERPRISE_LICENSE_TEXT
|
||||
extends:
|
||||
file: .config/docker-compose-base.yaml
|
||||
service: grafana
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/alexanderzobnin/grafana-zabbix
|
||||
|
||||
go 1.24.6
|
||||
go 1.25
|
||||
|
||||
// Go 1.24 enabled the post-quantum key exchange mechanism
|
||||
// X25519MLKEM768 by default. It can cause issues with some TLS servers
|
||||
|
||||
58
package.json
58
package.json
@@ -15,9 +15,9 @@
|
||||
"scripts": {
|
||||
"build": "webpack -c ./webpack.config.ts --env production",
|
||||
"dev": "webpack -w -c ./webpack.config.ts --env development",
|
||||
"e2e": "yarn playwright test",
|
||||
"e2e": "playwright test",
|
||||
"lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .",
|
||||
"lint:fix": "yarn run lint --fix",
|
||||
"lint:fix": "yarn run lint --fix && prettier --write --list-different .",
|
||||
"server": "docker compose up --build",
|
||||
"server:down": "docker compose --file ./devenv/default/docker-compose.yml down",
|
||||
"server:stop": "docker compose --file ./devenv/default/docker-compose.yml stop",
|
||||
@@ -29,24 +29,27 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.10.6",
|
||||
"@grafana/data": "10.4.8",
|
||||
"@grafana/plugin-ui": "^0.9.1",
|
||||
"@grafana/runtime": "10.4.8",
|
||||
"@grafana/schema": "10.4.8",
|
||||
"@grafana/ui": "10.4.2",
|
||||
"@grafana/data": "^12.1.0",
|
||||
"@grafana/i18n": "^12.1.0",
|
||||
"@grafana/plugin-ui": "^0.10.10",
|
||||
"@grafana/runtime": "^12.1.0",
|
||||
"@grafana/schema": "^12.1.0",
|
||||
"@grafana/ui": "^12.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"rxjs": "7.8.1",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"rxjs": "7.8.2",
|
||||
"tslib": "2.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@grafana/eslint-config": "^6.0.0",
|
||||
"@grafana/plugin-e2e": "^1.17.1",
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@changesets/cli": "^2.29.7",
|
||||
"@grafana/e2e-selectors": "12.1.0",
|
||||
"@grafana/eslint-config": "^8.0.0",
|
||||
"@grafana/plugin-e2e": "^2.1.12",
|
||||
"@grafana/tsconfig": "^2.0.0",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@stylistic/eslint-plugin-ts": "^2.9.0",
|
||||
"@swc/core": "^1.3.90",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"@swc/jest": "^0.2.26",
|
||||
@@ -57,16 +60,18 @@
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^20.8.7",
|
||||
"@types/react": "^18.2.25",
|
||||
"@types/react-router-dom": "^5.2.0",
|
||||
"@types/testing-library__jest-dom": "5.14.8",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"autoprefixer": "10.4.7",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"cspell": "6.13.3",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "8.42.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-deprecation": "^2.0.0",
|
||||
"eslint-plugin-jsdoc": "^46.8.2",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
@@ -76,6 +81,7 @@
|
||||
"fork-ts-checker-webpack-plugin": "^8.0.0",
|
||||
"glob": "^10.2.7",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"lodash": "4.17.21",
|
||||
@@ -92,21 +98,19 @@
|
||||
"replace-in-file-webpack-plugin": "^1.0.6",
|
||||
"sass": "1.63.2",
|
||||
"sass-loader": "13.3.1",
|
||||
"semver": "7.5.4",
|
||||
"semver": "^7.6.3",
|
||||
"style-loader": "3.3.3",
|
||||
"swc-loader": "^0.2.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "4.8.4",
|
||||
"typescript": "5.5.4",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-livereload-plugin": "^3.0.2",
|
||||
"webpack-remove-empty-scripts": "^1.0.1"
|
||||
"webpack-remove-empty-scripts": "^1.0.1",
|
||||
"webpack-subresource-integrity": "^5.1.0",
|
||||
"webpack-virtual-modules": "^0.6.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"uplot": "1.6.31",
|
||||
"prismjs": "1.30.0",
|
||||
"jackspeak": "2.1.1"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
"packageManager": "yarn@4.9.4"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@ func (ds *ZabbixDatasource) ZabbixAPIHandler(rw http.ResponseWriter, req *http.R
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
defer req.Body.Close()
|
||||
defer func() {
|
||||
if err := req.Body.Close(); err != nil {
|
||||
log.DefaultLogger.Warn("Error closing request body", "error", err)
|
||||
}
|
||||
}()
|
||||
if err != nil || len(body) == 0 {
|
||||
writeError(rw, http.StatusBadRequest, err)
|
||||
return
|
||||
@@ -74,7 +78,11 @@ func (ds *ZabbixDatasource) DBConnectionPostProcessingHandler(rw http.ResponseWr
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
defer req.Body.Close()
|
||||
defer func() {
|
||||
if err := req.Body.Close(); err != nil {
|
||||
log.DefaultLogger.Warn("Error closing request body", "error", err)
|
||||
}
|
||||
}()
|
||||
if err != nil || len(body) == 0 {
|
||||
writeError(rw, http.StatusBadRequest, err)
|
||||
return
|
||||
|
||||
@@ -142,7 +142,8 @@ func convertTrendToHistory(trend zabbix.Trend, valueType string) (zabbix.History
|
||||
}
|
||||
|
||||
func getTrendPointValue(point zabbix.TrendPoint, valueType string) (float64, error) {
|
||||
if valueType == "avg" || valueType == "min" || valueType == "max" || valueType == "count" {
|
||||
switch valueType {
|
||||
case "avg", "min", "max", "count":
|
||||
valueStr := point.ValueAvg
|
||||
switch valueType {
|
||||
case "min":
|
||||
@@ -158,7 +159,7 @@ func getTrendPointValue(point zabbix.TrendPoint, valueType string) (float64, err
|
||||
return 0, backend.DownstreamError(fmt.Errorf("error parsing trend value: %s", err))
|
||||
}
|
||||
return value, nil
|
||||
} else if valueType == "sum" {
|
||||
case "sum":
|
||||
avgStr := point.ValueAvg
|
||||
avg, err := strconv.ParseFloat(avgStr, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -40,7 +40,7 @@ func (ts TimeSeries) Align(interval time.Duration) TimeSeries {
|
||||
}
|
||||
}
|
||||
|
||||
if len(alignedTs) > 0 && alignedTs[len(alignedTs)-1].Time == pointFrameTs {
|
||||
if len(alignedTs) > 0 && alignedTs[len(alignedTs)-1].Time.Equal(pointFrameTs) {
|
||||
// Do not append points with the same timestamp
|
||||
alignedTs[len(alignedTs)-1] = TimePoint{Time: pointFrameTs, Value: point.Value}
|
||||
} else {
|
||||
|
||||
@@ -46,7 +46,7 @@ func (ts TimeSeries) GroupBy(interval time.Duration, aggFunc AggFunc) TimeSeries
|
||||
pointFrameTs = point.GetTimeFrame(interval)
|
||||
|
||||
// Iterate over points and push it into the frame if point time stamp fit the frame
|
||||
if pointFrameTs == frameTS {
|
||||
if pointFrameTs.Equal(frameTS) {
|
||||
frame = append(frame, point)
|
||||
} else if pointFrameTs.After(frameTS) {
|
||||
// If point outside frame, then we've done with current frame
|
||||
@@ -137,9 +137,10 @@ func Filter(series []*TimeSeriesData, n int, order string, aggFunc AggFunc) []*T
|
||||
maxN := int(math.Min(float64(n), float64(len(series))))
|
||||
filteredSeries := make([]*TimeSeriesData, maxN)
|
||||
for i := 0; i < maxN; i++ {
|
||||
if order == "top" {
|
||||
switch order {
|
||||
case "top":
|
||||
filteredSeries[i] = series[len(series)-1-i]
|
||||
} else if order == "bottom" {
|
||||
case "bottom":
|
||||
filteredSeries[i] = series[i]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,9 +383,10 @@ func (ds *Zabbix) GetAllItems(ctx context.Context, hostids []string, appids []st
|
||||
}
|
||||
|
||||
filter := params["filter"].(map[string]interface{})
|
||||
if itemtype == "num" {
|
||||
switch itemtype {
|
||||
case "num":
|
||||
filter["value_type"] = []int{0, 3}
|
||||
} else if itemtype == "text" {
|
||||
case "text":
|
||||
filter["value_type"] = []int{1, 2, 4}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ func (api *ZabbixAPI) request(ctx context.Context, method string, params ZabbixA
|
||||
|
||||
if auth != "" && version >= 72 {
|
||||
if api.dsSettings.BasicAuthEnabled {
|
||||
return nil, backend.DownstreamError(errors.New("Basic Auth is not supported for Zabbix v7.2 and later"))
|
||||
return nil, backend.DownstreamErrorf("basic auth is not supported for Zabbix v7.2 and later")
|
||||
}
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", auth))
|
||||
}
|
||||
@@ -223,7 +223,11 @@ func makeHTTPRequest(ctx context.Context, httpClient *http.Client, req *http.Req
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
defer func() {
|
||||
if err := res.Body.Close(); err != nil {
|
||||
log.DefaultLogger.Warn("Error closing response body", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
err = fmt.Errorf("request failed, status: %v", res.Status)
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestIntegrationZabbixAPI72(t *testing.T) {
|
||||
// Try to authenticate
|
||||
_, err = apiWithBasicAuth.Request(context.Background(), "hostgroup.get", params, zabbixVersion)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Basic Auth is not supported for Zabbix v7.2 and later")
|
||||
assert.Contains(t, err.Error(), "basic auth is not supported for Zabbix v7.2 and later")
|
||||
assert.True(t, backend.IsDownstreamError(err))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestIntegrationZabbixAPI74(t *testing.T) {
|
||||
// Try to authenticate
|
||||
_, err = apiWithBasicAuth.Request(context.Background(), "hostgroup.get", params, zabbixVersion)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Basic Auth is not supported for Zabbix v7.2 and later")
|
||||
assert.Contains(t, err.Error(), "basic auth is not supported for Zabbix v7.2 and later")
|
||||
assert.True(t, backend.IsDownstreamError(err))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface Props {
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const MetricPicker = ({ value, options, isLoading, width, onChange }: Props): JSX.Element => {
|
||||
export const MetricPicker = ({ value, options, isLoading, width, onChange }: Props) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState(value);
|
||||
const [filteredOptions, setFilteredOptions] = useState(options);
|
||||
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
selected?: number;
|
||||
}
|
||||
|
||||
export const MetricPickerMenu = ({ options, offset, minWidth, selected, onSelect }: Props): JSX.Element => {
|
||||
export const MetricPickerMenu = ({ options, offset, minWidth, selected, onSelect }: Props) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getSelectStyles(theme);
|
||||
const customStyles = useStyles2(getStyles(minWidth));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useState, FormEvent } from 'react';
|
||||
import React, { useState, FormEvent, ReactNode } from 'react';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
HorizontalGroup,
|
||||
@@ -71,7 +71,7 @@ export const QueryOptionsEditor = ({ queryType, queryOptions, onChange }: Props)
|
||||
};
|
||||
|
||||
const renderOptions = () => {
|
||||
const elements: JSX.Element[] = [];
|
||||
const elements: ReactNode[] = [];
|
||||
for (const key in queryOptions) {
|
||||
if (queryOptions.hasOwnProperty(key)) {
|
||||
const value = queryOptions[key];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DataFrame, dateTime, Field, FieldType } from '@grafana/data';
|
||||
import _ from 'lodash';
|
||||
import * as utils from './utils';
|
||||
import { DataFrame, Field, FieldType, ArrayVector, dateTime } from '@grafana/data';
|
||||
import { ProblemDTO, ZBXEvent, ZBXProblem, ZBXTrigger } from './types';
|
||||
import { ZabbixMetricsQuery } from './types/query';
|
||||
import { ZBXProblem, ZBXTrigger, ProblemDTO, ZBXEvent } from './types';
|
||||
import * as utils from './utils';
|
||||
|
||||
export function joinTriggersWithProblems(problems: ZBXProblem[], triggers: ZBXTrigger[]): ProblemDTO[] {
|
||||
const problemDTOList: ProblemDTO[] = [];
|
||||
@@ -210,7 +210,7 @@ export function toDataFrame(problems: any[], query: ZabbixMetricsQuery): DataFra
|
||||
const problemsField: Field<any> = {
|
||||
name: 'Problems',
|
||||
type: FieldType.other,
|
||||
values: new ArrayVector(problems),
|
||||
values: problems,
|
||||
config: {
|
||||
custom: {
|
||||
type: 'problems',
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import TableModel from 'grafana/app/core/table_model';
|
||||
import * as c from './constants';
|
||||
import * as utils from './utils';
|
||||
import {
|
||||
ArrayVector,
|
||||
DataFrame,
|
||||
dataFrameFromJSON,
|
||||
DataFrameJSON,
|
||||
@@ -16,8 +11,12 @@ import {
|
||||
TIME_SERIES_TIME_FIELD_NAME,
|
||||
TIME_SERIES_VALUE_FIELD_NAME,
|
||||
} from '@grafana/data';
|
||||
import { ZabbixMetricsQuery } from './types/query';
|
||||
import TableModel from 'grafana/app/core/table_model';
|
||||
import _ from 'lodash';
|
||||
import * as c from './constants';
|
||||
import { ZBXGroup, ZBXTrigger } from './types';
|
||||
import { ZabbixMetricsQuery } from './types/query';
|
||||
import * as utils from './utils';
|
||||
|
||||
/**
|
||||
* Convert Zabbix API history.get response to Grafana format
|
||||
@@ -113,14 +112,14 @@ export function seriesToDataFrame(
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: new ArrayVector<number>(datapoints.map((p) => p[c.DATAPOINT_TS])),
|
||||
values: datapoints.map((p) => p[c.DATAPOINT_TS]),
|
||||
};
|
||||
|
||||
let values: ArrayVector<number> | ArrayVector<string>;
|
||||
let values: number[] | string[];
|
||||
if (fieldType === FieldType.string) {
|
||||
values = new ArrayVector<string>(datapoints.map((p) => p[c.DATAPOINT_VALUE]));
|
||||
values = datapoints.map((p) => p[c.DATAPOINT_VALUE]);
|
||||
} else {
|
||||
values = new ArrayVector<number>(datapoints.map((p) => p[c.DATAPOINT_VALUE]));
|
||||
values = datapoints.map((p) => p[c.DATAPOINT_VALUE]);
|
||||
}
|
||||
|
||||
const valueFiled: Field = {
|
||||
@@ -360,8 +359,8 @@ export function alignFrames(data: MutableDataFrame[]): MutableDataFrame[] {
|
||||
|
||||
timestamps = missingTimestamps.concat(timestamps);
|
||||
values = missingValues.concat(values);
|
||||
timeField.values = new ArrayVector(timestamps);
|
||||
valueField.values = new ArrayVector(values);
|
||||
timeField.values = timestamps;
|
||||
valueField.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,7 +508,7 @@ export function handleSLIResponse(response: any, itservices: any[], target: Zabb
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: new ArrayVector<number>(timestamps),
|
||||
values: timestamps,
|
||||
};
|
||||
|
||||
const valueFields: Field[] = [];
|
||||
@@ -535,7 +534,7 @@ export function handleSLIResponse(response: any, itservices: any[], target: Zabb
|
||||
name: service ? service.name : serviceId,
|
||||
type: FieldType.number,
|
||||
config: {},
|
||||
values: new ArrayVector<number>(values[i]),
|
||||
values: values[i],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -568,7 +567,7 @@ export function handleMultiSLIResponse(response: any[], itservices: any[], slas:
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: new ArrayVector<number>(timestamps),
|
||||
values: timestamps,
|
||||
};
|
||||
|
||||
const valueFields: Field[] = [];
|
||||
@@ -602,7 +601,7 @@ export function handleMultiSLIResponse(response: any[], itservices: any[], slas:
|
||||
name,
|
||||
type: FieldType.number,
|
||||
config: {},
|
||||
values: new ArrayVector<number>(values[i]),
|
||||
values: values[i],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -636,7 +635,7 @@ export function handleServiceResponse(response: any, itservices: any[], target:
|
||||
name: service ? service.name : i,
|
||||
type: FieldType.number,
|
||||
config: {},
|
||||
values: new ArrayVector<number>([status]),
|
||||
values: [status],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -646,7 +645,7 @@ export function handleServiceResponse(response: any, itservices: any[], target:
|
||||
config: {
|
||||
custom: {},
|
||||
},
|
||||
values: new ArrayVector<number>([Date.now()]),
|
||||
values: [Date.now()],
|
||||
};
|
||||
|
||||
return new MutableDataFrame({
|
||||
@@ -694,8 +693,8 @@ function handleTriggersResponse(triggers: ZBXTrigger[], groups: ZBXGroup[], time
|
||||
name: `Count ${target.refId}`,
|
||||
refId: target.refId,
|
||||
fields: [
|
||||
{ name: TIME_SERIES_TIME_FIELD_NAME, type: FieldType.time, values: new ArrayVector([timeRange[1] * 1000]) },
|
||||
{ name: TIME_SERIES_VALUE_FIELD_NAME, type: FieldType.number, values: new ArrayVector([triggersCount]) },
|
||||
{ name: TIME_SERIES_TIME_FIELD_NAME, type: FieldType.time, values: [timeRange[1] * 1000] },
|
||||
{ name: TIME_SERIES_VALUE_FIELD_NAME, type: FieldType.number, values: [triggersCount] },
|
||||
],
|
||||
length: 1,
|
||||
});
|
||||
@@ -706,7 +705,7 @@ function handleTriggersResponse(triggers: ZBXTrigger[], groups: ZBXGroup[], time
|
||||
const frame = new MutableDataFrame({
|
||||
name: `Triggers ${target.refId}`,
|
||||
refId: target.refId,
|
||||
fields: [{ name: 'Host group', type: FieldType.string, values: new ArrayVector() }],
|
||||
fields: [{ name: 'Host group', type: FieldType.string, values: [] }],
|
||||
});
|
||||
|
||||
for (let i = c.TRIGGER_SEVERITY.length - 1; i >= 0; i--) {
|
||||
@@ -714,7 +713,7 @@ function handleTriggersResponse(triggers: ZBXTrigger[], groups: ZBXGroup[], time
|
||||
name: c.TRIGGER_SEVERITY[i].text,
|
||||
type: FieldType.number,
|
||||
config: { unit: 'none', decimals: 0 },
|
||||
values: new ArrayVector(),
|
||||
values: [],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
ArrayVector,
|
||||
DataFrame,
|
||||
dataFrameToJSON,
|
||||
Field,
|
||||
@@ -142,14 +141,14 @@ function handleInfluxHistoryResponse(results) {
|
||||
name: TIME_SERIES_TIME_FIELD_NAME,
|
||||
type: FieldType.time,
|
||||
config: {},
|
||||
values: new ArrayVector(tsBuffer),
|
||||
values: tsBuffer,
|
||||
};
|
||||
|
||||
const valueFiled: Field<number | null> = {
|
||||
name: influxSeries?.tags?.itemid,
|
||||
type: FieldType.number,
|
||||
config: {},
|
||||
values: new ArrayVector(valuesBuffer),
|
||||
values: valuesBuffer,
|
||||
};
|
||||
|
||||
frames.push(
|
||||
|
||||
@@ -117,8 +117,8 @@ export class SQLConnector extends DBConnector {
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
const results = response.data.results;
|
||||
if (results['A']) {
|
||||
const results = (response.data as { results?: any }).results;
|
||||
if (results && results['A']) {
|
||||
return results['A'].frames;
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@@ -15,7 +15,7 @@ const PROBLEM_EVENTS_LIMIT = 100;
|
||||
|
||||
interface ProblemsPanelProps extends PanelProps<ProblemsPanelOptions> {}
|
||||
|
||||
export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
|
||||
export const ProblemsPanel = (props: ProblemsPanelProps) => {
|
||||
const { data, options, timeRange, onOptionsChange } = props;
|
||||
const { layout, showTriggers, triggerSeverity, sortProblems } = options;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||
import { Button, Spinner, Modal, Select, stylesFactory, withTheme, Themeable, ButtonGroup } from '@grafana/ui';
|
||||
@@ -16,8 +16,8 @@ interface State {
|
||||
scriptOptions: Array<SelectableValue<string>>;
|
||||
script: ZBXScript;
|
||||
error: boolean;
|
||||
errorMessage: string | JSX.Element;
|
||||
result: string | JSX.Element;
|
||||
errorMessage: string | ReactNode;
|
||||
result: string | ReactNode;
|
||||
selectError: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TriggerSeverity } from '../types';
|
||||
|
||||
type Props = StandardEditorProps<TriggerSeverity[]>;
|
||||
|
||||
export const ProblemColorEditor = ({ value, onChange }: Props): JSX.Element => {
|
||||
export const ProblemColorEditor = ({ value, onChange }: Props) => {
|
||||
const onSeverityItemChange = (severity: TriggerSeverity) => {
|
||||
value.forEach((v, i) => {
|
||||
if (v.priority === severity.priority) {
|
||||
@@ -33,7 +33,7 @@ interface ProblemColorEditorRowProps {
|
||||
onChange: (value?: TriggerSeverity) => void;
|
||||
}
|
||||
|
||||
export const ProblemColorEditorRow = ({ value, onChange }: ProblemColorEditorRowProps): JSX.Element => {
|
||||
export const ProblemColorEditorRow = ({ value, onChange }: ProblemColorEditorRowProps) => {
|
||||
const onSeverityNameChange = (v: FormEvent<HTMLInputElement>) => {
|
||||
const newValue = v?.currentTarget?.value;
|
||||
if (newValue !== null) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://github.com/grafana/grafana/raw/main/docs/sources/developers/plugins/plugin.schema.json",
|
||||
"type": "app",
|
||||
"name": "Zabbix",
|
||||
"id": "alexanderzobnin-zabbix-app",
|
||||
@@ -65,8 +66,6 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=10.4.8",
|
||||
"grafanaVersion": "10.4",
|
||||
"plugins": []
|
||||
"grafanaDependency": ">=11.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user