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:
github-actions[bot]
2025-09-17 20:33:12 +02:00
committed by GitHub
parent e76741b453
commit b13d567eee
54 changed files with 15452 additions and 10339 deletions

View File

@@ -0,0 +1,5 @@
---
'grafana-zabbix': major
---
Bump grafana dependency to 11.6.0

View File

@@ -1,3 +1,3 @@
{ {
"version": "4.2.1" "version": "5.26.4"
} }

View File

@@ -2,7 +2,7 @@
* ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
* *
* In order to extend the configuration follow the steps in * 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"], "extends": ["@grafana/eslint-config"],
@@ -12,14 +12,19 @@
}, },
"overrides": [ "overrides": [
{ {
"plugins": ["deprecation"],
"files": ["src/**/*.{ts,tsx}"], "files": ["src/**/*.{ts,tsx}"],
"rules": { "rules": {
"deprecation/deprecation": "warn" "@typescript-eslint/no-deprecated": "warn"
}, },
"parserOptions": { "parserOptions": {
"project": "./tsconfig.json" "project": "./tsconfig.json"
} }
},
{
"files": ["./tests/**/*"],
"rules": {
"react-hooks/rules-of-hooks": "off"
}
} }
] ]
} }

View File

@@ -3,14 +3,52 @@ ARG grafana_image=grafana-enterprise
FROM grafana/${grafana_image}:${grafana_version} 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 # 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 # Do NOT enable these settings in a public facing / production grafana instance
ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin" 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" ENV GF_AUTH_BASIC_ENABLED "false"
# Set development mode so plugins can be loaded without the need to sign # Set development mode so plugins can be loaded without the need to sign
ENV GF_DEFAULT_APP_MODE "development" 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 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 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"]

View File

@@ -106,9 +106,9 @@ We are going to use [`webpack-merge`](https://github.com/survivejs/webpack-merge
// webpack.config.ts // webpack.config.ts
import type { Configuration } from 'webpack'; import type { Configuration } from 'webpack';
import { merge } from 'webpack-merge'; 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); const baseConfig = await grafanaConfig(env);
return merge(baseConfig, { return merge(baseConfig, {
@@ -151,14 +151,15 @@ version: '3.7'
services: services:
grafana: grafana:
container_name: 'myorg-basic-app' extends:
file: .config/docker-compose-base.yaml
service: grafana
build: build:
context: ./.config
args: args:
grafana_version: ${GRAFANA_VERSION:-9.1.2} grafana_version: ${GRAFANA_VERSION:-9.1.2}
grafana_image: ${GRAFANA_IMAGE:-grafana} 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.
--- ---

View 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
View 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

View File

@@ -2,15 +2,18 @@
* ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
* *
* In order to extend the configuration follow the steps in * 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 '@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 // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(global, 'matchMedia', { Object.defineProperty(global, 'matchMedia', {
writable: true, writable: true,
value: jest.fn().mockImplementation((query) => ({ value: (query) => ({
matches: false, matches: false,
media: query, media: query,
onchange: null, onchange: null,
@@ -19,7 +22,7 @@ Object.defineProperty(global, 'matchMedia', {
addEventListener: jest.fn(), addEventListener: jest.fn(),
removeEventListener: jest.fn(), removeEventListener: jest.fn(),
dispatchEvent: jest.fn(), dispatchEvent: jest.fn(),
})), }),
}); });
HTMLCanvasElement.prototype.getContext = () => {}; HTMLCanvasElement.prototype.getContext = () => {};

View File

@@ -2,7 +2,7 @@
* ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
* *
* In order to extend the configuration follow the steps in * 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'); 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 // 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 // ES module without it being transformed first. ./config/README.md#esm-errors-with-jest
transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)], transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)],
watchPathIgnorePatterns: ['<rootDir>/node_modules', '<rootDir>/dist'],
}; };

View File

@@ -14,12 +14,18 @@ const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNam
const grafanaESModules = [ const grafanaESModules = [
'.pnpm', // Support using pnpm symlinked packages '.pnpm', // Support using pnpm symlinked packages
'@grafana/schema', '@grafana/schema',
'@wojtekmaj/date-utils',
'd3', 'd3',
'd3-color', 'd3-color',
'd3-force', 'd3-force',
'd3-interpolate', 'd3-interpolate',
'd3-scale-chromatic', 'd3-scale-chromatic',
'get-user-locale',
'marked',
'memoize',
'mimic-function',
'ol', 'ol',
'react-calendar',
'react-colorful', 'react-colorful',
'rxjs', 'rxjs',
'uuid', 'uuid',

View 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

View File

@@ -2,7 +2,7 @@
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. * THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
* *
* In order to extend the configuration follow the steps in * 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": { "compilerOptions": {

37
.config/types/bundler-rules.d.ts vendored Normal file
View 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
View 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;
}

View 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));
}
}
}
);
});
}
}

View File

@@ -3,7 +3,7 @@ import process from 'process';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import { glob } from 'glob'; import { glob } from 'glob';
import { SOURCE_DIR } from './constants'; import { SOURCE_DIR } from './constants.ts';
export function isWSL() { export function isWSL() {
if (process.platform !== 'linux') { 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() { export function getPackageJson() {
return require(path.resolve(process.cwd(), 'package.json')); return loadJson(path.resolve(process.cwd(), 'package.json'));
} }
export function getPluginJson() { 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() { export function hasReadme() {
@@ -35,7 +45,7 @@ export function hasReadme() {
// Support bundling nested plugins by finding all plugin.json files in src directory // Support bundling nested plugins by finding all plugin.json files in src directory
// then checking for a sibling module.[jt]sx? file. // 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 pluginsJson = await glob('**/src/**/plugin.json', { absolute: true });
const plugins = await Promise.all( const plugins = await Promise.all(
@@ -45,14 +55,14 @@ export async function getEntries(): Promise<Record<string, string>> {
}) })
); );
return plugins.reduce((result, modules) => { return plugins.reduce<Record<string, string>>((result, modules) => {
return modules.reduce((result, module) => { return modules.reduce((innerResult, module) => {
const pluginPath = path.dirname(module); const pluginPath = path.dirname(module);
const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, ''); const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, '');
const entryName = pluginName === '' ? 'module' : `${pluginName}/module`; const entryName = pluginName === '' ? 'module' : `${pluginName}/module`;
result[entryName] = module; innerResult[entryName] = module;
return result; return innerResult;
}, result); }, result);
}, {}); }, {});
} }

View File

@@ -2,28 +2,50 @@
* ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
* *
* In order to extend the configuration follow the steps in * 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 CopyWebpackPlugin from 'copy-webpack-plugin';
import ESLintPlugin from 'eslint-webpack-plugin'; import ESLintPlugin from 'eslint-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import LiveReloadPlugin from 'webpack-livereload-plugin';
import path from 'path'; import path from 'path';
import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin'; 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 { BuildModeWebpackPlugin } from './BuildModeWebpackPlugin.ts';
import { SOURCE_DIR, DIST_DIR } from './constants'; import { DIST_DIR, SOURCE_DIR } from './constants.ts';
import { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, hasReadme, isWSL } from './utils.ts';
const pluginJson = getPluginJson(); 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 = { const baseConfig: Configuration = {
cache: { cache: {
type: 'filesystem', type: 'filesystem',
buildDependencies: { 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(), entry: await getEntries(),
externals: [ externals: [
// Required for dynamic publicPath resolution
{ 'amd-module': 'module' },
'lodash', 'lodash',
'jquery', 'jquery',
'moment', 'moment',
@@ -49,21 +73,21 @@ const config = async (env): Promise<Configuration> => {
'react-redux', 'react-redux',
'redux', 'redux',
'rxjs', 'rxjs',
'i18next',
'react-router', 'react-router',
'react-router-dom',
'd3', 'd3',
'angular', 'angular',
'@grafana/ui', /^@grafana\/ui/i,
'@grafana/runtime', /^@grafana\/runtime/i,
'@grafana/data', /^@grafana\/data/i,
// Mark legacy SDK imports as external if their name starts with the "grafana/" prefix // Mark legacy SDK imports as external if their name starts with the "grafana/" prefix
({ request }, callback) => { ({ request }, callback) => {
const prefix = 'grafana/'; const prefix = 'grafana/';
const hasPrefix = (request) => request.indexOf(prefix) === 0; const hasPrefix = (request: string) => request.indexOf(prefix) === 0;
const stripPrefix = (request) => request.substr(prefix.length); const stripPrefix = (request: string) => request.substr(prefix.length);
if (hasPrefix(request)) { if (request && hasPrefix(request)) {
return callback(undefined, stripPrefix(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', mode: env.production ? 'production' : 'development',
module: { module: {
rules: [ 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)/, exclude: /(node_modules)/,
test: /\.[tj]sx?$/, test: /\.[tj]sx?$/,
@@ -82,7 +123,7 @@ const config = async (env): Promise<Configuration> => {
loader: 'swc-loader', loader: 'swc-loader',
options: { options: {
jsc: { jsc: {
baseUrl: path.resolve(__dirname, 'src'), baseUrl: path.resolve(process.cwd(), SOURCE_DIR),
target: 'es2015', target: 'es2015',
loose: false, loose: false,
parser: { parser: {
@@ -107,9 +148,6 @@ const config = async (env): Promise<Configuration> => {
test: /\.(png|jpe?g|gif|svg)$/, test: /\.(png|jpe?g|gif|svg)$/,
type: 'asset/resource', type: 'asset/resource',
generator: { generator: {
// Keep publicPath relative for host.com/grafana/ deployments
publicPath: `public/plugins/${pluginJson.id}/img/`,
outputPath: 'img/',
filename: Boolean(env.production) ? '[hash][ext]' : '[file]', 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+)?$/, test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/,
type: 'asset/resource', type: 'asset/resource',
generator: { generator: {
// Keep publicPath relative for host.com/grafana/ deployments filename: Boolean(env.production) ? '[hash][ext]' : '[file]',
publicPath: `public/plugins/${pluginJson.id}/fonts/`,
outputPath: 'fonts/',
filename: Boolean(env.production) ? '[hash][ext]' : '[name][ext]',
}, },
}, },
], ],
}, },
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: { output: {
clean: { clean: {
keep: new RegExp(`(.*?_(amd64|arm(64)?)(.exe)?|go_plugin_build_manifest)`), keep: new RegExp(`(.*?_(amd64|arm(64)?)(.exe)?|go_plugin_build_manifest)`),
}, },
filename: '[name].js', filename: '[name].js',
chunkFilename: env.production ? '[name].js?_cache=[contenthash]' : '[name].js',
library: { library: {
type: 'amd', type: 'amd',
}, },
path: path.resolve(process.cwd(), DIST_DIR), path: path.resolve(process.cwd(), DIST_DIR),
publicPath: `public/plugins/${pluginJson.id}/`, publicPath: `public/plugins/${pluginJson.id}/`,
uniqueName: pluginJson.id, uniqueName: pluginJson.id,
crossOriginLoading: 'anonymous',
}, },
plugins: [ 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({ new CopyWebpackPlugin({
patterns: [ patterns: [
// If src/README.md exists use it; otherwise the root README // 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: 'plugin.json', to: '.' },
{ from: '../LICENSE', to: '.' }, { from: '../LICENSE', to: '.' },
{ from: '../CHANGELOG.md', to: '.', force: true }, { from: '../CHANGELOG.md', to: '.', force: true },
{ from: '**/*.json', to: '.' }, // TODO<Add an error for checking the basic structure of the repo> { from: '**/*.json', to: '.' },
{ from: '**/*.svg', to: '.', noErrorOnMissing: true }, // Optional { from: '**/*.svg', to: '.', noErrorOnMissing: true },
{ from: '**/*.png', to: '.', noErrorOnMissing: true }, // Optional { from: '**/*.png', to: '.', noErrorOnMissing: true },
{ from: '**/*.html', to: '.', noErrorOnMissing: true }, // Optional { from: '**/*.html', to: '.', noErrorOnMissing: true },
{ from: 'img/**/*', to: '.', noErrorOnMissing: true }, // Optional { from: 'img/**/*', to: '.', noErrorOnMissing: true },
{ from: 'libs/**/*', to: '.', noErrorOnMissing: true }, // Optional { from: 'libs/**/*', to: '.', noErrorOnMissing: true },
{ from: 'static/**/*', to: '.', noErrorOnMissing: true }, // Optional { from: 'static/**/*', to: '.', noErrorOnMissing: true },
{ from: '**/query_help.md', to: '.', noErrorOnMissing: true }, // Optional { from: '**/query_help.md', to: '.', noErrorOnMissing: true },
], ],
}), }),
// Replace certain template-variables in the README and plugin.json // Replace certain template-variables in the README and plugin.json
@@ -166,7 +228,7 @@ const config = async (env): Promise<Configuration> => {
rules: [ rules: [
{ {
search: /\%VERSION\%/g, search: /\%VERSION\%/g,
replace: getPackageJson().version, replace: pluginVersion,
}, },
{ {
search: /\%TODAY\%/g, search: /\%TODAY\%/g,
@@ -179,6 +241,9 @@ const config = async (env): Promise<Configuration> => {
], ],
}, },
]), ]),
new SubresourceIntegrityPlugin({
hashFuncNames: ['sha256'],
}),
...(env.development ...(env.development
? [ ? [
new LiveReloadPlugin(), new LiveReloadPlugin(),
@@ -192,6 +257,7 @@ const config = async (env): Promise<Configuration> => {
new ESLintPlugin({ new ESLintPlugin({
extensions: ['.ts', '.tsx'], extensions: ['.ts', '.tsx'],
lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files
failOnError: Boolean(env.production),
}), }),
] ]
: []), : []),

7
.cprc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"features": {
"bundleGrafanaUI": false,
"useReactRouterV6": true,
"useExperimentalRspack": false
}
}

View File

@@ -1,21 +1,28 @@
name: Latest Grafana API compatibility check name: Compatibility check
on: [pull_request] on: [push]
permissions: {}
jobs: jobs:
compatibilitycheck: 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 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable --prefer-offline run: yarn install
- name: Build plugin - name: Build plugin
run: yarn build run: yarn build
- name: Compatibility check - 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'

View File

@@ -29,9 +29,10 @@ jobs:
name: CD name: CD
uses: grafana/plugin-ci-workflows/.github/workflows/cd.yml@main uses: grafana/plugin-ci-workflows/.github/workflows/cd.yml@main
with: with:
go-version: '1.24' go-version: '1.25'
golangci-lint-version: '1.64.6' golangci-lint-version: '2.4.0'
branch: ${{ github.event.inputs.branch }} branch: ${{ github.event.inputs.branch }}
environment: ${{ github.event.inputs.environment }} environment: ${{ github.event.inputs.environment }}
docs-only: ${{ fromJSON(github.event.inputs.docs-only) }} docs-only: ${{ fromJSON(github.event.inputs.docs-only) }}
github-draft-release: false
run-playwright: true run-playwright: true

View File

@@ -14,7 +14,7 @@ jobs:
name: CI name: CI
uses: grafana/plugin-ci-workflows/.github/workflows/ci.yml@main uses: grafana/plugin-ci-workflows/.github/workflows/ci.yml@main
with: with:
go-version: '1.24' go-version: '1.25'
golangci-lint-version: '1.64.6' golangci-lint-version: '2.4.0'
plugin-version-suffix: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }} plugin-version-suffix: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
run-playwright: true run-playwright: true

8
.gitignore vendored
View File

@@ -55,3 +55,11 @@ provisioning/
# SSL certificates # SSL certificates
devenv/nginx/nginx.crt devenv/nginx/nginx.crt
devenv/nginx/nginx.key 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

File diff suppressed because one or more lines are too long

3
.yarnrc.yml Normal file
View File

@@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.4.cjs

View File

@@ -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

View File

@@ -1,10 +0,0 @@
app_mode = development
[log]
level = debug
[plugins]
enable_alpha = true
[plugin.alexanderzobnin-zabbix-datasource]
path = /grafana-zabbix

View File

@@ -15,13 +15,12 @@ services:
# Grafana # Grafana
grafana: grafana:
image: grafana/grafana:main extends:
ports: file: ../../.config/docker-compose-base.yaml
- '3001:3000' service: grafana
volumes: volumes:
- ../..:/grafana-zabbix - ../..:/grafana-zabbix
- ../dashboards:/devenv/dashboards - ../dashboards:/devenv/dashboards
- ../grafana.ini:/etc/grafana/grafana.ini:ro
- '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml' - '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml'
- '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml' - '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml'

View File

@@ -15,13 +15,12 @@ services:
# Grafana # Grafana
grafana: grafana:
image: grafana/grafana:main extends:
ports: file: ../../.config/docker-compose-base.yaml
- '3001:3000' service: grafana
volumes: volumes:
- ../..:/grafana-zabbix - ../..:/grafana-zabbix
- ../dashboards:/devenv/dashboards - ../dashboards:/devenv/dashboards
- ../grafana.ini:/etc/grafana/grafana.ini:ro
- '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml' - '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml'
- '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml' - '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml'

View File

@@ -15,13 +15,12 @@ services:
# Grafana # Grafana
grafana: grafana:
image: grafana/grafana:main extends:
ports: file: ../../.config/docker-compose-base.yaml
- '3001:3000' service: grafana
volumes: volumes:
- ../..:/grafana-zabbix - ../..:/grafana-zabbix
- ../dashboards:/devenv/dashboards - ../dashboards:/devenv/dashboards
- ../grafana.ini:/etc/grafana/grafana.ini:ro
- '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml' - '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml'
- '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml' - '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml'

View File

@@ -1,13 +1,12 @@
services: services:
# Grafana # Grafana
grafana: grafana:
image: grafana/grafana:main extends:
ports: file: ../../.config/docker-compose-base.yaml
- '3001:3000' service: grafana
volumes: volumes:
- ../..:/grafana-zabbix - ../..:/grafana-zabbix
- ../dashboards:/devenv/dashboards - ../dashboards:/devenv/dashboards
- ../grafana.ini:/etc/grafana/grafana.ini:ro
- '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml' - '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml'
- '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml' - '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml'

View File

@@ -1,13 +1,12 @@
services: services:
# Grafana # Grafana
grafana: grafana:
image: grafana/grafana:main extends:
ports: file: ../../.config/docker-compose-base.yaml
- '3001:3000' service: grafana
volumes: volumes:
- ../..:/grafana-zabbix - ../..:/grafana-zabbix
- ../dashboards:/devenv/dashboards - ../dashboards:/devenv/dashboards
- ../grafana.ini:/etc/grafana/grafana.ini:ro
- '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml' - '../datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml'
- '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml' - '../dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml'

View File

@@ -1,17 +1,5 @@
version: '3.0'
services: services:
grafana: grafana:
container_name: 'grafana-zabbix' extends:
image: grafana/grafana-enterprise:${GF_VERSION:-main} file: .config/docker-compose-base.yaml
ports: service: grafana
- 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

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/alexanderzobnin/grafana-zabbix module github.com/alexanderzobnin/grafana-zabbix
go 1.24.6 go 1.25
// Go 1.24 enabled the post-quantum key exchange mechanism // Go 1.24 enabled the post-quantum key exchange mechanism
// X25519MLKEM768 by default. It can cause issues with some TLS servers // X25519MLKEM768 by default. It can cause issues with some TLS servers

View File

@@ -15,9 +15,9 @@
"scripts": { "scripts": {
"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": "yarn playwright test", "e2e": "playwright test",
"lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .", "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": "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",
"server:stop": "docker compose --file ./devenv/default/docker-compose.yml stop", "server:stop": "docker compose --file ./devenv/default/docker-compose.yml stop",
@@ -29,24 +29,27 @@
}, },
"dependencies": { "dependencies": {
"@emotion/css": "11.10.6", "@emotion/css": "11.10.6",
"@grafana/data": "10.4.8", "@grafana/data": "^12.1.0",
"@grafana/plugin-ui": "^0.9.1", "@grafana/i18n": "^12.1.0",
"@grafana/runtime": "10.4.8", "@grafana/plugin-ui": "^0.10.10",
"@grafana/schema": "10.4.8", "@grafana/runtime": "^12.1.0",
"@grafana/ui": "10.4.2", "@grafana/schema": "^12.1.0",
"@grafana/ui": "^12.1.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^6.22.0",
"rxjs": "7.8.1", "rxjs": "7.8.2",
"tslib": "2.5.3" "tslib": "2.5.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.21.4", "@babel/core": "^7.21.4",
"@changesets/cli": "^2.27.12", "@changesets/cli": "^2.29.7",
"@grafana/eslint-config": "^6.0.0", "@grafana/e2e-selectors": "12.1.0",
"@grafana/plugin-e2e": "^1.17.1", "@grafana/eslint-config": "^8.0.0",
"@grafana/tsconfig": "^1.2.0-rc1", "@grafana/plugin-e2e": "^2.1.12",
"@playwright/test": "^1.50.1", "@grafana/tsconfig": "^2.0.0",
"@playwright/test": "^1.52.0",
"@stylistic/eslint-plugin-ts": "^2.9.0",
"@swc/core": "^1.3.90", "@swc/core": "^1.3.90",
"@swc/helpers": "^0.5.0", "@swc/helpers": "^0.5.0",
"@swc/jest": "^0.2.26", "@swc/jest": "^0.2.26",
@@ -57,16 +60,18 @@
"@types/jest": "^29.5.0", "@types/jest": "^29.5.0",
"@types/lodash": "^4.14.194", "@types/lodash": "^4.14.194",
"@types/node": "^20.8.7", "@types/node": "^20.8.7",
"@types/react": "^18.2.25",
"@types/react-router-dom": "^5.2.0", "@types/react-router-dom": "^5.2.0",
"@types/testing-library__jest-dom": "5.14.8", "@types/testing-library__jest-dom": "5.14.8",
"@typescript-eslint/eslint-plugin": "5.59.5", "@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "5.59.5", "@typescript-eslint/parser": "^8.3.0",
"autoprefixer": "10.4.7", "autoprefixer": "10.4.7",
"clean-webpack-plugin": "^0.1.19", "clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"cspell": "6.13.3", "cspell": "6.13.3",
"css-loader": "^6.7.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-deprecation": "^2.0.0",
"eslint-plugin-jsdoc": "^46.8.2", "eslint-plugin-jsdoc": "^46.8.2",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
@@ -76,6 +81,7 @@
"fork-ts-checker-webpack-plugin": "^8.0.0", "fork-ts-checker-webpack-plugin": "^8.0.0",
"glob": "^10.2.7", "glob": "^10.2.7",
"identity-obj-proxy": "3.0.0", "identity-obj-proxy": "3.0.0",
"imports-loader": "^5.0.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0", "jest-environment-jsdom": "^29.5.0",
"lodash": "4.17.21", "lodash": "4.17.21",
@@ -92,21 +98,19 @@
"replace-in-file-webpack-plugin": "^1.0.6", "replace-in-file-webpack-plugin": "^1.0.6",
"sass": "1.63.2", "sass": "1.63.2",
"sass-loader": "13.3.1", "sass-loader": "13.3.1",
"semver": "7.5.4", "semver": "^7.6.3",
"style-loader": "3.3.3", "style-loader": "3.3.3",
"swc-loader": "^0.2.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", "tsconfig-paths": "^4.2.0",
"typescript": "4.8.4", "typescript": "5.5.4",
"webpack": "^5.94.0", "webpack": "^5.94.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-livereload-plugin": "^3.0.2", "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": { "packageManager": "yarn@4.9.4"
"uplot": "1.6.31",
"prismjs": "1.30.0",
"jackspeak": "2.1.1"
},
"packageManager": "yarn@1.22.19"
} }

View File

@@ -33,7 +33,11 @@ func (ds *ZabbixDatasource) ZabbixAPIHandler(rw http.ResponseWriter, req *http.R
} }
body, err := io.ReadAll(req.Body) 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 { if err != nil || len(body) == 0 {
writeError(rw, http.StatusBadRequest, err) writeError(rw, http.StatusBadRequest, err)
return return
@@ -74,7 +78,11 @@ func (ds *ZabbixDatasource) DBConnectionPostProcessingHandler(rw http.ResponseWr
} }
body, err := io.ReadAll(req.Body) 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 { if err != nil || len(body) == 0 {
writeError(rw, http.StatusBadRequest, err) writeError(rw, http.StatusBadRequest, err)
return return

View File

@@ -142,7 +142,8 @@ func convertTrendToHistory(trend zabbix.Trend, valueType string) (zabbix.History
} }
func getTrendPointValue(point zabbix.TrendPoint, valueType string) (float64, error) { 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 valueStr := point.ValueAvg
switch valueType { switch valueType {
case "min": 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 0, backend.DownstreamError(fmt.Errorf("error parsing trend value: %s", err))
} }
return value, nil return value, nil
} else if valueType == "sum" { case "sum":
avgStr := point.ValueAvg avgStr := point.ValueAvg
avg, err := strconv.ParseFloat(avgStr, 64) avg, err := strconv.ParseFloat(avgStr, 64)
if err != nil { if err != nil {

View File

@@ -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 // Do not append points with the same timestamp
alignedTs[len(alignedTs)-1] = TimePoint{Time: pointFrameTs, Value: point.Value} alignedTs[len(alignedTs)-1] = TimePoint{Time: pointFrameTs, Value: point.Value}
} else { } else {

View File

@@ -46,7 +46,7 @@ func (ts TimeSeries) GroupBy(interval time.Duration, aggFunc AggFunc) TimeSeries
pointFrameTs = point.GetTimeFrame(interval) pointFrameTs = point.GetTimeFrame(interval)
// Iterate over points and push it into the frame if point time stamp fit the frame // 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) frame = append(frame, point)
} else if pointFrameTs.After(frameTS) { } else if pointFrameTs.After(frameTS) {
// If point outside frame, then we've done with current frame // 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)))) maxN := int(math.Min(float64(n), float64(len(series))))
filteredSeries := make([]*TimeSeriesData, maxN) filteredSeries := make([]*TimeSeriesData, maxN)
for i := 0; i < maxN; i++ { for i := 0; i < maxN; i++ {
if order == "top" { switch order {
case "top":
filteredSeries[i] = series[len(series)-1-i] filteredSeries[i] = series[len(series)-1-i]
} else if order == "bottom" { case "bottom":
filteredSeries[i] = series[i] filteredSeries[i] = series[i]
} }
} }

View File

@@ -383,9 +383,10 @@ func (ds *Zabbix) GetAllItems(ctx context.Context, hostids []string, appids []st
} }
filter := params["filter"].(map[string]interface{}) filter := params["filter"].(map[string]interface{})
if itemtype == "num" { switch itemtype {
case "num":
filter["value_type"] = []int{0, 3} filter["value_type"] = []int{0, 3}
} else if itemtype == "text" { case "text":
filter["value_type"] = []int{1, 2, 4} filter["value_type"] = []int{1, 2, 4}
} }

View File

@@ -118,7 +118,7 @@ func (api *ZabbixAPI) request(ctx context.Context, method string, params ZabbixA
if auth != "" && version >= 72 { if auth != "" && version >= 72 {
if api.dsSettings.BasicAuthEnabled { 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)) 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 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 { if res.StatusCode != http.StatusOK {
err = fmt.Errorf("request failed, status: %v", res.Status) err = fmt.Errorf("request failed, status: %v", res.Status)

View File

@@ -155,7 +155,7 @@ func TestIntegrationZabbixAPI72(t *testing.T) {
// Try to authenticate // Try to authenticate
_, err = apiWithBasicAuth.Request(context.Background(), "hostgroup.get", params, zabbixVersion) _, err = apiWithBasicAuth.Request(context.Background(), "hostgroup.get", params, zabbixVersion)
require.Error(t, err) 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)) assert.True(t, backend.IsDownstreamError(err))
}) })
} }

View File

@@ -155,7 +155,7 @@ func TestIntegrationZabbixAPI74(t *testing.T) {
// Try to authenticate // Try to authenticate
_, err = apiWithBasicAuth.Request(context.Background(), "hostgroup.get", params, zabbixVersion) _, err = apiWithBasicAuth.Request(context.Background(), "hostgroup.get", params, zabbixVersion)
require.Error(t, err) 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)) assert.True(t, backend.IsDownstreamError(err))
}) })
} }

View File

@@ -13,7 +13,7 @@ export interface Props {
onChange: (value: string) => void; 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 [isOpen, setOpen] = useState(false);
const [query, setQuery] = useState(value); const [query, setQuery] = useState(value);
const [filteredOptions, setFilteredOptions] = useState(options); const [filteredOptions, setFilteredOptions] = useState(options);

View File

@@ -12,7 +12,7 @@ interface Props {
selected?: number; 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 theme = useTheme2();
const styles = getSelectStyles(theme); const styles = getSelectStyles(theme);
const customStyles = useStyles2(getStyles(minWidth)); const customStyles = useStyles2(getStyles(minWidth));

View File

@@ -1,5 +1,5 @@
import { css } from '@emotion/css'; 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 { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { import {
HorizontalGroup, HorizontalGroup,
@@ -71,7 +71,7 @@ export const QueryOptionsEditor = ({ queryType, queryOptions, onChange }: Props)
}; };
const renderOptions = () => { const renderOptions = () => {
const elements: JSX.Element[] = []; const elements: ReactNode[] = [];
for (const key in queryOptions) { for (const key in queryOptions) {
if (queryOptions.hasOwnProperty(key)) { if (queryOptions.hasOwnProperty(key)) {
const value = queryOptions[key]; const value = queryOptions[key];

View File

@@ -1,8 +1,8 @@
import { DataFrame, dateTime, Field, FieldType } from '@grafana/data';
import _ from 'lodash'; import _ from 'lodash';
import * as utils from './utils'; import { ProblemDTO, ZBXEvent, ZBXProblem, ZBXTrigger } from './types';
import { DataFrame, Field, FieldType, ArrayVector, dateTime } from '@grafana/data';
import { ZabbixMetricsQuery } from './types/query'; 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[] { export function joinTriggersWithProblems(problems: ZBXProblem[], triggers: ZBXTrigger[]): ProblemDTO[] {
const problemDTOList: ProblemDTO[] = []; const problemDTOList: ProblemDTO[] = [];
@@ -210,7 +210,7 @@ export function toDataFrame(problems: any[], query: ZabbixMetricsQuery): DataFra
const problemsField: Field<any> = { const problemsField: Field<any> = {
name: 'Problems', name: 'Problems',
type: FieldType.other, type: FieldType.other,
values: new ArrayVector(problems), values: problems,
config: { config: {
custom: { custom: {
type: 'problems', type: 'problems',

View File

@@ -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 { import {
ArrayVector,
DataFrame, DataFrame,
dataFrameFromJSON, dataFrameFromJSON,
DataFrameJSON, DataFrameJSON,
@@ -16,8 +11,12 @@ import {
TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_TIME_FIELD_NAME,
TIME_SERIES_VALUE_FIELD_NAME, TIME_SERIES_VALUE_FIELD_NAME,
} from '@grafana/data'; } 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 { ZBXGroup, ZBXTrigger } from './types';
import { ZabbixMetricsQuery } from './types/query';
import * as utils from './utils';
/** /**
* Convert Zabbix API history.get response to Grafana format * Convert Zabbix API history.get response to Grafana format
@@ -113,14 +112,14 @@ export function seriesToDataFrame(
config: { config: {
custom: {}, 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) { if (fieldType === FieldType.string) {
values = new ArrayVector<string>(datapoints.map((p) => p[c.DATAPOINT_VALUE])); values = datapoints.map((p) => p[c.DATAPOINT_VALUE]);
} else { } else {
values = new ArrayVector<number>(datapoints.map((p) => p[c.DATAPOINT_VALUE])); values = datapoints.map((p) => p[c.DATAPOINT_VALUE]);
} }
const valueFiled: Field = { const valueFiled: Field = {
@@ -360,8 +359,8 @@ export function alignFrames(data: MutableDataFrame[]): MutableDataFrame[] {
timestamps = missingTimestamps.concat(timestamps); timestamps = missingTimestamps.concat(timestamps);
values = missingValues.concat(values); values = missingValues.concat(values);
timeField.values = new ArrayVector(timestamps); timeField.values = timestamps;
valueField.values = new ArrayVector(values); valueField.values = values;
} }
} }
@@ -509,7 +508,7 @@ export function handleSLIResponse(response: any, itservices: any[], target: Zabb
config: { config: {
custom: {}, custom: {},
}, },
values: new ArrayVector<number>(timestamps), values: timestamps,
}; };
const valueFields: Field[] = []; const valueFields: Field[] = [];
@@ -535,7 +534,7 @@ export function handleSLIResponse(response: any, itservices: any[], target: Zabb
name: service ? service.name : serviceId, name: service ? service.name : serviceId,
type: FieldType.number, type: FieldType.number,
config: {}, config: {},
values: new ArrayVector<number>(values[i]), values: values[i],
}); });
} }
@@ -568,7 +567,7 @@ export function handleMultiSLIResponse(response: any[], itservices: any[], slas:
config: { config: {
custom: {}, custom: {},
}, },
values: new ArrayVector<number>(timestamps), values: timestamps,
}; };
const valueFields: Field[] = []; const valueFields: Field[] = [];
@@ -602,7 +601,7 @@ export function handleMultiSLIResponse(response: any[], itservices: any[], slas:
name, name,
type: FieldType.number, type: FieldType.number,
config: {}, 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, name: service ? service.name : i,
type: FieldType.number, type: FieldType.number,
config: {}, config: {},
values: new ArrayVector<number>([status]), values: [status],
}); });
} }
@@ -646,7 +645,7 @@ export function handleServiceResponse(response: any, itservices: any[], target:
config: { config: {
custom: {}, custom: {},
}, },
values: new ArrayVector<number>([Date.now()]), values: [Date.now()],
}; };
return new MutableDataFrame({ return new MutableDataFrame({
@@ -694,8 +693,8 @@ function handleTriggersResponse(triggers: ZBXTrigger[], groups: ZBXGroup[], time
name: `Count ${target.refId}`, name: `Count ${target.refId}`,
refId: target.refId, refId: target.refId,
fields: [ fields: [
{ name: TIME_SERIES_TIME_FIELD_NAME, type: FieldType.time, values: new ArrayVector([timeRange[1] * 1000]) }, { name: TIME_SERIES_TIME_FIELD_NAME, type: FieldType.time, values: [timeRange[1] * 1000] },
{ name: TIME_SERIES_VALUE_FIELD_NAME, type: FieldType.number, values: new ArrayVector([triggersCount]) }, { name: TIME_SERIES_VALUE_FIELD_NAME, type: FieldType.number, values: [triggersCount] },
], ],
length: 1, length: 1,
}); });
@@ -706,7 +705,7 @@ function handleTriggersResponse(triggers: ZBXTrigger[], groups: ZBXGroup[], time
const frame = new MutableDataFrame({ const frame = new MutableDataFrame({
name: `Triggers ${target.refId}`, name: `Triggers ${target.refId}`,
refId: 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--) { 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, name: c.TRIGGER_SEVERITY[i].text,
type: FieldType.number, type: FieldType.number,
config: { unit: 'none', decimals: 0 }, config: { unit: 'none', decimals: 0 },
values: new ArrayVector(), values: [],
}); });
} }

View File

@@ -1,5 +1,4 @@
import { import {
ArrayVector,
DataFrame, DataFrame,
dataFrameToJSON, dataFrameToJSON,
Field, Field,
@@ -142,14 +141,14 @@ function handleInfluxHistoryResponse(results) {
name: TIME_SERIES_TIME_FIELD_NAME, name: TIME_SERIES_TIME_FIELD_NAME,
type: FieldType.time, type: FieldType.time,
config: {}, config: {},
values: new ArrayVector(tsBuffer), values: tsBuffer,
}; };
const valueFiled: Field<number | null> = { const valueFiled: Field<number | null> = {
name: influxSeries?.tags?.itemid, name: influxSeries?.tags?.itemid,
type: FieldType.number, type: FieldType.number,
config: {}, config: {},
values: new ArrayVector(valuesBuffer), values: valuesBuffer,
}; };
frames.push( frames.push(

View File

@@ -117,8 +117,8 @@ export class SQLConnector extends DBConnector {
}, },
}) })
.then((response) => { .then((response) => {
const results = response.data.results; const results = (response.data as { results?: any }).results;
if (results['A']) { if (results && results['A']) {
return results['A'].frames; return results['A'].frames;
} else { } else {
return null; return null;

View File

@@ -15,7 +15,7 @@ const PROBLEM_EVENTS_LIMIT = 100;
interface ProblemsPanelProps extends PanelProps<ProblemsPanelOptions> {} interface ProblemsPanelProps extends PanelProps<ProblemsPanelOptions> {}
export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => { export const ProblemsPanel = (props: ProblemsPanelProps) => {
const { data, options, timeRange, onOptionsChange } = props; const { data, options, timeRange, onOptionsChange } = props;
const { layout, showTriggers, triggerSeverity, sortProblems } = options; const { layout, showTriggers, triggerSeverity, sortProblems } = options;

View File

@@ -1,4 +1,4 @@
import React, { PureComponent } from 'react'; import React, { PureComponent, ReactNode } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { Button, Spinner, Modal, Select, stylesFactory, withTheme, Themeable, ButtonGroup } from '@grafana/ui'; import { Button, Spinner, Modal, Select, stylesFactory, withTheme, Themeable, ButtonGroup } from '@grafana/ui';
@@ -16,8 +16,8 @@ interface State {
scriptOptions: Array<SelectableValue<string>>; scriptOptions: Array<SelectableValue<string>>;
script: ZBXScript; script: ZBXScript;
error: boolean; error: boolean;
errorMessage: string | JSX.Element; errorMessage: string | ReactNode;
result: string | JSX.Element; result: string | ReactNode;
selectError: string; selectError: string;
loading: boolean; loading: boolean;
} }

View File

@@ -5,7 +5,7 @@ import { TriggerSeverity } from '../types';
type Props = StandardEditorProps<TriggerSeverity[]>; type Props = StandardEditorProps<TriggerSeverity[]>;
export const ProblemColorEditor = ({ value, onChange }: Props): JSX.Element => { export const ProblemColorEditor = ({ value, onChange }: Props) => {
const onSeverityItemChange = (severity: TriggerSeverity) => { const onSeverityItemChange = (severity: TriggerSeverity) => {
value.forEach((v, i) => { value.forEach((v, i) => {
if (v.priority === severity.priority) { if (v.priority === severity.priority) {
@@ -33,7 +33,7 @@ interface ProblemColorEditorRowProps {
onChange: (value?: TriggerSeverity) => void; onChange: (value?: TriggerSeverity) => void;
} }
export const ProblemColorEditorRow = ({ value, onChange }: ProblemColorEditorRowProps): JSX.Element => { export const ProblemColorEditorRow = ({ value, onChange }: ProblemColorEditorRowProps) => {
const onSeverityNameChange = (v: FormEvent<HTMLInputElement>) => { const onSeverityNameChange = (v: FormEvent<HTMLInputElement>) => {
const newValue = v?.currentTarget?.value; const newValue = v?.currentTarget?.value;
if (newValue !== null) { if (newValue !== null) {

View File

@@ -1,4 +1,5 @@
{ {
"$schema": "https://github.com/grafana/grafana/raw/main/docs/sources/developers/plugins/plugin.schema.json",
"type": "app", "type": "app",
"name": "Zabbix", "name": "Zabbix",
"id": "alexanderzobnin-zabbix-app", "id": "alexanderzobnin-zabbix-app",
@@ -65,8 +66,6 @@
} }
], ],
"dependencies": { "dependencies": {
"grafanaDependency": ">=10.4.8", "grafanaDependency": ">=11.6.0"
"grafanaVersion": "10.4",
"plugins": []
} }
} }

24065
yarn.lock

File diff suppressed because it is too large Load Diff