├── .config ├── .cprc.json ├── .eslintrc ├── .prettierrc.js ├── Dockerfile ├── README.md ├── entrypoint.sh ├── jest-setup.js ├── jest.config.js ├── jest │ ├── mocks │ │ └── react-inlinesvg.tsx │ └── utils.js ├── supervisord │ └── supervisord.conf ├── tsconfig.json ├── types │ └── custom.d.ts └── webpack │ ├── constants.ts │ ├── publicPath.ts │ ├── utils.ts │ └── webpack.config.ts ├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .eslintrc.cjs ├── .github ├── build │ └── action.yml └── workflows │ ├── ci.yml │ ├── cla.yml │ ├── release.yml │ └── warn.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── docker-compose.yaml ├── jest-setup.js ├── jest.config.js ├── package-lock.json ├── package.json ├── playwright.config.ts ├── src ├── DataSource.ts ├── RequestSpec.ts ├── api_utils.ts ├── backend │ ├── rest.ts │ ├── types.ts │ └── validate.ts ├── img │ ├── checkmk_logo.svg │ └── screenshot.png ├── module.ts ├── plugin.json ├── settings.ts ├── types.ts ├── ui │ ├── ConfigEditor.tsx │ ├── QueryEditor.tsx │ ├── VariableQueryEditor.tsx │ ├── autocomplete.ts │ ├── components.tsx │ ├── filters.tsx │ └── utils.ts └── utils.ts ├── tests ├── .dockerignore ├── Dockerfile ├── checkmk-docker-hooks │ ├── post-create │ │ └── pre-configure-checkmk.sh │ └── post-start │ │ ├── generate_random_rrd_data.py │ │ ├── localhost_grafana0 │ │ ├── localhost_grafana1 │ │ ├── post-configure-checkmk.py │ │ └── post-configure-checkmk.sh ├── docker-compose.yml ├── e2e │ ├── config.ts │ ├── constants.ts │ ├── lib │ │ ├── checkmk_rest_api.ts │ │ ├── grafana_rest_api.ts │ │ └── util.ts │ ├── pom │ │ ├── CMKRawQueryEditorPage.ts │ │ ├── CmkBasePage.ts │ │ ├── CmkCEEQueryEditorPage.ts │ │ ├── CmkDataSourceConfigPage.ts │ │ └── CmkQueryEditorPage.ts │ └── tests │ │ ├── datasource.test.ts │ │ ├── e2e.test.ts │ │ ├── global.setup.ts │ │ └── global.teardown.ts └── unit │ ├── CloudEdition.test.tsx │ ├── DataSource.test.ts │ ├── RequestSpec.test.ts │ ├── ui │ ├── CheckMkAsyncSelect.test.tsx │ └── QueryEditor.test.tsx │ └── utils.test.ts ├── tsconfig.json ├── tsfmt.json ├── utils ├── converter.py └── install_latest.sh └── webpack.config.ts /.config/.cprc.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.16.2" 3 | } 4 | -------------------------------------------------------------------------------- /.config/.eslintrc: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-eslint-config 6 | */ 7 | { 8 | "extends": ["@grafana/eslint-config"], 9 | "root": true, 10 | "rules": { 11 | "react/prop-types": "off" 12 | }, 13 | "overrides": [ 14 | { 15 | "plugins": ["deprecation"], 16 | "files": ["src/**/*.{ts,tsx}"], 17 | "rules": { 18 | "deprecation/deprecation": "warn" 19 | }, 20 | "parserOptions": { 21 | "project": "./tsconfig.json" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.config/.prettierrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in .config/README.md 5 | */ 6 | 7 | module.exports = { 8 | endOfLine: 'auto', 9 | printWidth: 120, 10 | trailingComma: 'es5', 11 | semi: true, 12 | jsxSingleQuote: false, 13 | singleQuote: true, 14 | useTabs: false, 15 | tabWidth: 2, 16 | }; 17 | -------------------------------------------------------------------------------- /.config/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG grafana_version=latest 2 | ARG grafana_image=grafana-enterprise 3 | 4 | FROM grafana/${grafana_image}:${grafana_version} 5 | 6 | ARG development=false 7 | ARG TARGETARCH 8 | 9 | 10 | ENV DEV "${development}" 11 | 12 | # Make it as simple as possible to access the grafana instance for development purposes 13 | # Do NOT enable these settings in a public facing / production grafana instance 14 | ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin" 15 | ENV GF_AUTH_ANONYMOUS_ENABLED "true" 16 | ENV GF_AUTH_BASIC_ENABLED "false" 17 | # Set development mode so plugins can be loaded without the need to sign 18 | ENV GF_DEFAULT_APP_MODE "development" 19 | 20 | 21 | LABEL maintainer="Grafana Labs " 22 | 23 | ENV GF_PATHS_HOME="/usr/share/grafana" 24 | WORKDIR $GF_PATHS_HOME 25 | 26 | USER root 27 | 28 | # Installing supervisor and inotify-tools 29 | RUN if [ "${development}" = "true" ]; then \ 30 | if grep -i -q alpine /etc/issue; then \ 31 | apk add supervisor inotify-tools git; \ 32 | elif grep -i -q ubuntu /etc/issue; then \ 33 | DEBIAN_FRONTEND=noninteractive && \ 34 | apt-get update && \ 35 | apt-get install -y supervisor inotify-tools git && \ 36 | rm -rf /var/lib/apt/lists/*; \ 37 | else \ 38 | echo 'ERROR: Unsupported base image' && /bin/false; \ 39 | fi \ 40 | fi 41 | 42 | COPY supervisord/supervisord.conf /etc/supervisor.d/supervisord.ini 43 | COPY supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf 44 | 45 | 46 | 47 | # Inject livereload script into grafana index.html 48 | RUN sed -i 's|||g' /usr/share/grafana/public/views/index.html 49 | 50 | 51 | COPY entrypoint.sh /entrypoint.sh 52 | RUN chmod +x /entrypoint.sh 53 | ENTRYPOINT ["/entrypoint.sh"] 54 | -------------------------------------------------------------------------------- /.config/README.md: -------------------------------------------------------------------------------- 1 | # Default build configuration by Grafana 2 | 3 | **This is an auto-generated directory and is not intended to be changed! ⚠️** 4 | 5 | The `.config/` directory holds basic configuration for the different tools 6 | that are used to develop, test and build the project. In order to make it updates easier we ask you to 7 | not edit files in this folder to extend configuration. 8 | 9 | ## How to extend the basic configs? 10 | 11 | Bear in mind that you are doing it at your own risk, and that extending any of the basic configuration can lead 12 | to issues around working with the project. 13 | 14 | ### Extending the ESLint config 15 | 16 | Edit the `.eslintrc` file in the project root in order to extend the ESLint configuration. 17 | 18 | **Example:** 19 | 20 | ```json 21 | { 22 | "extends": "./.config/.eslintrc", 23 | "rules": { 24 | "react/prop-types": "off" 25 | } 26 | } 27 | ``` 28 | 29 | --- 30 | 31 | ### Extending the Prettier config 32 | 33 | Edit the `.prettierrc.js` file in the project root in order to extend the Prettier configuration. 34 | 35 | **Example:** 36 | 37 | ```javascript 38 | module.exports = { 39 | // Prettier configuration provided by Grafana scaffolding 40 | ...require('./.config/.prettierrc.js'), 41 | 42 | semi: false, 43 | }; 44 | ``` 45 | 46 | --- 47 | 48 | ### Extending the Jest config 49 | 50 | There are two configuration in the project root that belong to Jest: `jest-setup.js` and `jest.config.js`. 51 | 52 | **`jest-setup.js`:** A file that is run before each test file in the suite is executed. We are using it to 53 | set up the Jest DOM for the testing library and to apply some polyfills. ([link to Jest docs](https://jestjs.io/docs/configuration#setupfilesafterenv-array)) 54 | 55 | **`jest.config.js`:** The main Jest configuration file that extends the Grafana recommended setup. ([link to Jest docs](https://jestjs.io/docs/configuration)) 56 | 57 | #### ESM errors with Jest 58 | 59 | A common issue with the current jest config involves importing an npm package that only offers an ESM build. These packages cause jest to error with `SyntaxError: Cannot use import statement outside a module`. To work around this, we provide a list of known packages to pass to the `[transformIgnorePatterns](https://jestjs.io/docs/configuration#transformignorepatterns-arraystring)` jest configuration property. If need be, this can be extended in the following way: 60 | 61 | ```javascript 62 | process.env.TZ = 'UTC'; 63 | const { grafanaESModules, nodeModulesToTransform } = require('./config/jest/utils'); 64 | 65 | module.exports = { 66 | // Jest configuration provided by Grafana 67 | ...require('./.config/jest.config'), 68 | // Inform jest to only transform specific node_module packages. 69 | transformIgnorePatterns: [nodeModulesToTransform([...grafanaESModules, 'packageName'])], 70 | }; 71 | ``` 72 | 73 | --- 74 | 75 | ### Extending the TypeScript config 76 | 77 | Edit the `tsconfig.json` file in the project root in order to extend the TypeScript configuration. 78 | 79 | **Example:** 80 | 81 | ```json 82 | { 83 | "extends": "./.config/tsconfig.json", 84 | "compilerOptions": { 85 | "preserveConstEnums": true 86 | } 87 | } 88 | ``` 89 | 90 | --- 91 | 92 | ### Extending the Webpack config 93 | 94 | Follow these steps to extend the basic Webpack configuration that lives under `.config/`: 95 | 96 | #### 1. Create a new Webpack configuration file 97 | 98 | Create a new config file that is going to extend the basic one provided by Grafana. 99 | It can live in the project root, e.g. `webpack.config.ts`. 100 | 101 | #### 2. Merge the basic config provided by Grafana and your custom setup 102 | 103 | We are going to use [`webpack-merge`](https://github.com/survivejs/webpack-merge) for this. 104 | 105 | ```typescript 106 | // webpack.config.ts 107 | import type { Configuration } from 'webpack'; 108 | import { merge } from 'webpack-merge'; 109 | import grafanaConfig from './.config/webpack/webpack.config'; 110 | 111 | const config = async (env): Promise => { 112 | const baseConfig = await grafanaConfig(env); 113 | 114 | return merge(baseConfig, { 115 | // Add custom config here... 116 | output: { 117 | asyncChunks: true, 118 | }, 119 | }); 120 | }; 121 | 122 | export default config; 123 | ``` 124 | 125 | #### 3. Update the `package.json` to use the new Webpack config 126 | 127 | We need to update the `scripts` in the `package.json` to use the extended Webpack configuration. 128 | 129 | **Update for `build`:** 130 | 131 | ```diff 132 | -"build": "webpack -c ./.config/webpack/webpack.config.ts --env production", 133 | +"build": "webpack -c ./webpack.config.ts --env production", 134 | ``` 135 | 136 | **Update for `dev`:** 137 | 138 | ```diff 139 | -"dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development", 140 | +"dev": "webpack -w -c ./webpack.config.ts --env development", 141 | ``` 142 | 143 | ### Configure grafana image to use when running docker 144 | 145 | By default, `grafana-enterprise` will be used as the docker image for all docker related commands. If you want to override this behavior, simply alter the `docker-compose.yaml` by adding the following build arg `grafana_image`. 146 | 147 | **Example:** 148 | 149 | ```yaml 150 | version: '3.7' 151 | 152 | services: 153 | grafana: 154 | container_name: 'myorg-basic-app' 155 | build: 156 | context: ./.config 157 | args: 158 | grafana_version: ${GRAFANA_VERSION:-9.1.2} 159 | grafana_image: ${GRAFANA_IMAGE:-grafana} 160 | ``` 161 | 162 | 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. 163 | 164 | --- 165 | -------------------------------------------------------------------------------- /.config/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "${DEV}" = "false" ]; then 4 | echo "Starting test mode" 5 | exec /run.sh 6 | fi 7 | 8 | echo "Starting development mode" 9 | 10 | if grep -i -q alpine /etc/issue; then 11 | exec /usr/bin/supervisord -c /etc/supervisord.conf 12 | elif grep -i -q ubuntu /etc/issue; then 13 | exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf 14 | else 15 | echo 'ERROR: Unsupported base image' 16 | exit 1 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /.config/jest-setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config 6 | */ 7 | 8 | import '@testing-library/jest-dom'; 9 | import { TextEncoder, TextDecoder } from 'util'; 10 | 11 | Object.assign(global, { TextDecoder, TextEncoder }); 12 | 13 | // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom 14 | Object.defineProperty(global, 'matchMedia', { 15 | writable: true, 16 | value: jest.fn().mockImplementation((query) => ({ 17 | matches: false, 18 | media: query, 19 | onchange: null, 20 | addListener: jest.fn(), // deprecated 21 | removeListener: jest.fn(), // deprecated 22 | addEventListener: jest.fn(), 23 | removeEventListener: jest.fn(), 24 | dispatchEvent: jest.fn(), 25 | })), 26 | }); 27 | 28 | HTMLCanvasElement.prototype.getContext = () => {}; 29 | -------------------------------------------------------------------------------- /.config/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config 6 | */ 7 | 8 | const path = require('path'); 9 | const { grafanaESModules, nodeModulesToTransform } = require('./jest/utils'); 10 | 11 | module.exports = { 12 | moduleNameMapper: { 13 | '\\.(css|scss|sass)$': 'identity-obj-proxy', 14 | 'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'), 15 | }, 16 | modulePaths: ['/src'], 17 | setupFilesAfterEnv: ['/jest-setup.js'], 18 | testEnvironment: 'jest-environment-jsdom', 19 | testMatch: [ 20 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 21 | '/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', 22 | '/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', 23 | ], 24 | transform: { 25 | '^.+\\.(t|j)sx?$': [ 26 | '@swc/jest', 27 | { 28 | sourceMaps: 'inline', 29 | jsc: { 30 | parser: { 31 | syntax: 'typescript', 32 | tsx: true, 33 | decorators: false, 34 | dynamicImport: true, 35 | }, 36 | }, 37 | }, 38 | ], 39 | }, 40 | // Jest will throw `Cannot use import statement outside module` if it tries to load an 41 | // ES module without it being transformed first. ./config/README.md#esm-errors-with-jest 42 | transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)], 43 | }; 44 | -------------------------------------------------------------------------------- /.config/jest/mocks/react-inlinesvg.tsx: -------------------------------------------------------------------------------- 1 | // Due to the grafana/ui Icon component making fetch requests to 2 | // `/public/img/icon/.svg` we need to mock react-inlinesvg to prevent 3 | // the failed fetch requests from displaying errors in console. 4 | 5 | import React from 'react'; 6 | 7 | type Callback = (...args: any[]) => void; 8 | 9 | export interface StorageItem { 10 | content: string; 11 | queue: Callback[]; 12 | status: string; 13 | } 14 | 15 | export const cacheStore: { [key: string]: StorageItem } = Object.create(null); 16 | 17 | const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/; 18 | 19 | const InlineSVG = ({ src }: { src: string }) => { 20 | // testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`) 21 | const testId = src.replace(SVG_FILE_NAME_REGEX, '$2'); 22 | return ; 23 | }; 24 | 25 | export default InlineSVG; 26 | -------------------------------------------------------------------------------- /.config/jest/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in .config/README.md 5 | */ 6 | 7 | /* 8 | * This utility function is useful in combination with jest `transformIgnorePatterns` config 9 | * to transform specific packages (e.g.ES modules) in a projects node_modules folder. 10 | */ 11 | const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNames.join('|')})\/.*)`; 12 | 13 | // Array of known nested grafana package dependencies that only bundle an ESM version 14 | const grafanaESModules = [ 15 | '.pnpm', // Support using pnpm symlinked packages 16 | '@grafana/schema', 17 | 'd3', 18 | 'd3-color', 19 | 'd3-force', 20 | 'd3-interpolate', 21 | 'd3-scale-chromatic', 22 | 'ol', 23 | 'react-colorful', 24 | 'rxjs', 25 | 'uuid', 26 | ]; 27 | 28 | module.exports = { 29 | nodeModulesToTransform, 30 | grafanaESModules, 31 | }; 32 | -------------------------------------------------------------------------------- /.config/supervisord/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | 5 | [program:grafana] 6 | user=root 7 | directory=/var/lib/grafana 8 | command=/run.sh 9 | stdout_logfile=/dev/fd/1 10 | stdout_logfile_maxbytes=0 11 | redirect_stderr=true 12 | killasgroup=true 13 | stopasgroup=true 14 | autostart=true 15 | 16 | -------------------------------------------------------------------------------- /.config/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-typescript-config 6 | */ 7 | { 8 | "compilerOptions": { 9 | "alwaysStrict": true, 10 | "declaration": false, 11 | "rootDir": "../src", 12 | "baseUrl": "../src", 13 | "typeRoots": ["../node_modules/@types"], 14 | "resolveJsonModule": true 15 | }, 16 | "ts-node": { 17 | "compilerOptions": { 18 | "module": "commonjs", 19 | "target": "es5", 20 | "esModuleInterop": true 21 | }, 22 | "transpileOnly": true 23 | }, 24 | "include": ["../src", "./types"], 25 | "extends": "@grafana/tsconfig" 26 | } 27 | -------------------------------------------------------------------------------- /.config/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | // Image declarations 2 | declare module '*.gif' { 3 | const src: string; 4 | export default src; 5 | } 6 | 7 | declare module '*.jpg' { 8 | const src: string; 9 | export default src; 10 | } 11 | 12 | declare module '*.jpeg' { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.png' { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module '*.webp' { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module '*.svg' { 28 | const content: string; 29 | export default content; 30 | } 31 | 32 | // Font declarations 33 | declare module '*.woff'; 34 | declare module '*.woff2'; 35 | declare module '*.eot'; 36 | declare module '*.ttf'; 37 | declare module '*.otf'; 38 | -------------------------------------------------------------------------------- /.config/webpack/constants.ts: -------------------------------------------------------------------------------- 1 | export const SOURCE_DIR = 'src'; 2 | export const DIST_DIR = 'dist'; 3 | -------------------------------------------------------------------------------- /.config/webpack/publicPath.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * This file dynamically sets the public path at runtime based on the location of the plugin's AMD module. 5 | * It relies on the magic `module` which is defined by the AMD loader. 6 | * https://github.com/requirejs/requirejs/wiki/Differences-between-the-simplified-CommonJS-wrapper-and-standard-AMD-define#module 7 | * 8 | * We fallback to the plugin root so that older versions of Grafana will continue to load the plugin correctly. 9 | */ 10 | 11 | // @ts-nocheck 12 | import amdMetaModule from 'amd-module'; 13 | 14 | __webpack_public_path__ = 15 | amdMetaModule && amdMetaModule.uri 16 | ? amdMetaModule.uri.slice(0, amdMetaModule.uri.lastIndexOf('/') + 1) 17 | : 'public/plugins/checkmk-cmkgrafanadatasource2-datasource/'; 18 | -------------------------------------------------------------------------------- /.config/webpack/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import process from 'process'; 3 | import os from 'os'; 4 | import path from 'path'; 5 | import { glob } from 'glob'; 6 | import { SOURCE_DIR } from './constants'; 7 | 8 | export function isWSL() { 9 | if (process.platform !== 'linux') { 10 | return false; 11 | } 12 | 13 | if (os.release().toLowerCase().includes('microsoft')) { 14 | return true; 15 | } 16 | 17 | try { 18 | return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); 19 | } catch { 20 | return false; 21 | } 22 | } 23 | 24 | export function getPackageJson() { 25 | return require(path.resolve(process.cwd(), 'package.json')); 26 | } 27 | 28 | export function getPluginJson() { 29 | return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`)); 30 | } 31 | 32 | export function getCPConfigVersion() { 33 | const cprcJson = path.resolve(__dirname, '../', '.cprc.json'); 34 | return fs.existsSync(cprcJson) ? require(cprcJson).version : { version: 'unknown' }; 35 | } 36 | 37 | export function hasReadme() { 38 | return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md')); 39 | } 40 | 41 | // Support bundling nested plugins by finding all plugin.json files in src directory 42 | // then checking for a sibling module.[jt]sx? file. 43 | export async function getEntries(): Promise> { 44 | const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true }); 45 | 46 | const plugins = await Promise.all( 47 | pluginsJson.map((pluginJson) => { 48 | const folder = path.dirname(pluginJson); 49 | return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true }); 50 | }) 51 | ); 52 | 53 | return plugins.reduce((result, modules) => { 54 | return modules.reduce((result, module) => { 55 | const pluginPath = path.dirname(module); 56 | const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, ''); 57 | const entryName = pluginName === '' ? 'module' : `${pluginName}/module`; 58 | 59 | result[entryName] = module; 60 | return result; 61 | }, result); 62 | }, {}); 63 | } 64 | -------------------------------------------------------------------------------- /.config/webpack/webpack.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-webpack-config 6 | */ 7 | 8 | import CopyWebpackPlugin from 'copy-webpack-plugin'; 9 | import ESLintPlugin from 'eslint-webpack-plugin'; 10 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 11 | import LiveReloadPlugin from 'webpack-livereload-plugin'; 12 | import path from 'path'; 13 | import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin'; 14 | import TerserPlugin from 'terser-webpack-plugin'; 15 | import { type Configuration, BannerPlugin } from 'webpack'; 16 | 17 | import { getPackageJson, getPluginJson, hasReadme, getEntries, isWSL, getCPConfigVersion } from './utils'; 18 | import { SOURCE_DIR, DIST_DIR } from './constants'; 19 | 20 | const pluginJson = getPluginJson(); 21 | const cpVersion = getCPConfigVersion(); 22 | 23 | const config = async (env): Promise => { 24 | const baseConfig: Configuration = { 25 | cache: { 26 | type: 'filesystem', 27 | buildDependencies: { 28 | config: [__filename], 29 | }, 30 | }, 31 | 32 | context: path.join(process.cwd(), SOURCE_DIR), 33 | 34 | devtool: env.production ? 'source-map' : 'eval-source-map', 35 | 36 | entry: await getEntries(), 37 | 38 | externals: [ 39 | // Required for dynamic publicPath resolution 40 | { 'amd-module': 'module' }, 41 | 'lodash', 42 | 'jquery', 43 | 'moment', 44 | 'slate', 45 | 'emotion', 46 | '@emotion/react', 47 | '@emotion/css', 48 | 'prismjs', 49 | 'slate-plain-serializer', 50 | '@grafana/slate-react', 51 | 'react', 52 | 'react-dom', 53 | 'react-redux', 54 | 'redux', 55 | 'rxjs', 56 | 'react-router', 57 | 'react-router-dom', 58 | 'd3', 59 | 'angular', 60 | '@grafana/ui', 61 | '@grafana/runtime', 62 | '@grafana/data', 63 | 64 | // Mark legacy SDK imports as external if their name starts with the "grafana/" prefix 65 | ({ request }, callback) => { 66 | const prefix = 'grafana/'; 67 | const hasPrefix = (request) => request.indexOf(prefix) === 0; 68 | const stripPrefix = (request) => request.substr(prefix.length); 69 | 70 | if (hasPrefix(request)) { 71 | return callback(undefined, stripPrefix(request)); 72 | } 73 | 74 | callback(); 75 | }, 76 | ], 77 | 78 | // Support WebAssembly according to latest spec - makes WebAssembly module async 79 | experiments: { 80 | asyncWebAssembly: true, 81 | }, 82 | 83 | mode: env.production ? 'production' : 'development', 84 | 85 | module: { 86 | rules: [ 87 | { 88 | exclude: /(node_modules)/, 89 | test: /\.[tj]sx?$/, 90 | use: { 91 | loader: 'swc-loader', 92 | options: { 93 | jsc: { 94 | baseUrl: path.resolve(process.cwd(), SOURCE_DIR), 95 | target: 'es2015', 96 | loose: false, 97 | parser: { 98 | syntax: 'typescript', 99 | tsx: true, 100 | decorators: false, 101 | dynamicImport: true, 102 | }, 103 | }, 104 | }, 105 | }, 106 | }, 107 | { 108 | test: /src\/(?:.*\/)?module\.tsx?$/, 109 | use: [ 110 | { 111 | loader: 'imports-loader', 112 | options: { 113 | imports: `side-effects ${path.join(__dirname, 'publicPath.ts')}`, 114 | }, 115 | }, 116 | ], 117 | }, 118 | { 119 | test: /\.css$/, 120 | use: ['style-loader', 'css-loader'], 121 | }, 122 | { 123 | test: /\.s[ac]ss$/, 124 | use: ['style-loader', 'css-loader', 'sass-loader'], 125 | }, 126 | { 127 | test: /\.(png|jpe?g|gif|svg)$/, 128 | type: 'asset/resource', 129 | generator: { 130 | filename: Boolean(env.production) ? '[hash][ext]' : '[file]', 131 | }, 132 | }, 133 | { 134 | test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/, 135 | type: 'asset/resource', 136 | generator: { 137 | filename: Boolean(env.production) ? '[hash][ext]' : '[file]', 138 | }, 139 | }, 140 | ], 141 | }, 142 | 143 | optimization: { 144 | minimize: Boolean(env.production), 145 | minimizer: [ 146 | new TerserPlugin({ 147 | terserOptions: { 148 | format: { 149 | comments: (_, { type, value }) => type === 'comment2' && value.trim().startsWith('[create-plugin]'), 150 | }, 151 | }, 152 | }), 153 | ], 154 | }, 155 | 156 | output: { 157 | clean: { 158 | keep: new RegExp(`(.*?_(amd64|arm(64)?)(.exe)?|go_plugin_build_manifest)`), 159 | }, 160 | filename: '[name].js', 161 | library: { 162 | type: 'amd', 163 | }, 164 | path: path.resolve(process.cwd(), DIST_DIR), 165 | publicPath: `public/plugins/${pluginJson.id}/`, 166 | uniqueName: pluginJson.id, 167 | }, 168 | 169 | plugins: [ 170 | // Insert create plugin version information into the bundle 171 | new BannerPlugin({ 172 | banner: '/* [create-plugin] version: ' + cpVersion + ' */', 173 | raw: true, 174 | entryOnly: true, 175 | }), 176 | new CopyWebpackPlugin({ 177 | patterns: [ 178 | // If src/README.md exists use it; otherwise the root README 179 | // To `compiler.options.output` 180 | { from: hasReadme() ? 'README.md' : '../README.md', to: '.', force: true }, 181 | { from: 'plugin.json', to: '.' }, 182 | { from: '../LICENSE', to: '.' }, 183 | { from: '../CHANGELOG.md', to: '.', force: true }, 184 | { from: '**/*.json', to: '.' }, // TODO 185 | { from: '**/*.svg', to: '.', noErrorOnMissing: true }, // Optional 186 | { from: '**/*.png', to: '.', noErrorOnMissing: true }, // Optional 187 | { from: '**/*.html', to: '.', noErrorOnMissing: true }, // Optional 188 | { from: 'img/**/*', to: '.', noErrorOnMissing: true }, // Optional 189 | { from: 'libs/**/*', to: '.', noErrorOnMissing: true }, // Optional 190 | { from: 'static/**/*', to: '.', noErrorOnMissing: true }, // Optional 191 | { from: '**/query_help.md', to: '.', noErrorOnMissing: true }, // Optional 192 | ], 193 | }), 194 | // Replace certain template-variables in the README and plugin.json 195 | new ReplaceInFileWebpackPlugin([ 196 | { 197 | dir: DIST_DIR, 198 | files: ['plugin.json', 'README.md'], 199 | rules: [ 200 | { 201 | search: /\%VERSION\%/g, 202 | replace: getPackageJson().version, 203 | }, 204 | { 205 | search: /\%TODAY\%/g, 206 | replace: new Date().toISOString().substring(0, 10), 207 | }, 208 | { 209 | search: /\%PLUGIN_ID\%/g, 210 | replace: pluginJson.id, 211 | }, 212 | ], 213 | }, 214 | ]), 215 | ...(env.development 216 | ? [ 217 | new LiveReloadPlugin(), 218 | new ForkTsCheckerWebpackPlugin({ 219 | async: Boolean(env.development), 220 | issue: { 221 | include: [{ file: '**/*.{ts,tsx}' }], 222 | }, 223 | typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') }, 224 | }), 225 | new ESLintPlugin({ 226 | extensions: ['.ts', '.tsx'], 227 | lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files 228 | }), 229 | ] 230 | : []), 231 | ], 232 | 233 | resolve: { 234 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 235 | // handle resolving "rootDir" paths 236 | modules: [path.resolve(process.cwd(), 'src'), 'node_modules'], 237 | unsafeCache: true, 238 | }, 239 | }; 240 | 241 | if (isWSL()) { 242 | baseConfig.watchOptions = { 243 | poll: 3000, 244 | ignored: /node_modules/, 245 | }; 246 | } 247 | 248 | return baseConfig; 249 | }; 250 | 251 | export default config; 252 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | playwright-report/ 3 | tests/docker/ 4 | tests/results/ -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.config/.eslintrc", 3 | "env": { 4 | "node": true, 5 | "commonjs": true 6 | } 7 | } -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', '@grafana/eslint-config'], 6 | }; 7 | -------------------------------------------------------------------------------- /.github/build/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | inputs: 3 | release: 4 | description: 'Boolean to specify if this is a release (true), or test build (false)' 5 | required: true 6 | signed: 7 | description: 'Boolean to specify if this build should be signed' 8 | required: true 9 | outputs: 10 | plugin_path: 11 | description: 'path to the plugin archive' 12 | value: '${{ steps.metadata.outputs.archive }}' 13 | plugin_checksum_path: 14 | description: 'checksum of plugin archive' 15 | value: '${{ steps.metadata.outputs.archive-checksum }}' 16 | permissions: 17 | id-token: write 18 | contents: read 19 | attestations: write 20 | runs: 21 | using: 'composite' 22 | steps: 23 | - name: Install dependencies 24 | shell: bash 25 | run: | 26 | npm ci 27 | sudo apt-get install jq 28 | 29 | - name: Adapt version of plugin to indicate this is not a official release 30 | shell: bash 31 | run: | 32 | VERSION=$(jq -r .version package.json) 33 | # increase patch version, otherwise grafana plugin validator will fail: 34 | VERSION=$(echo "$VERSION" | perl -pe 's/^((\d+\.)*)(\d+)(.*)$/$1.($3+1).$4/e') 35 | VERSION=$VERSION-testbuild.${{ github.run_id }} 36 | jq ".version |= \"$VERSION\"" ./package.json > ./package.new.json 37 | mv ./package.new.json ./package.json 38 | if: ${{ inputs.release == 'false' }} 39 | 40 | - name: Build signed plugin and test frontend 41 | shell: bash 42 | run: | 43 | npm run build 44 | npm run sign 45 | if: ${{ inputs.signed == 'true'}} 46 | 47 | - name: Get plugin metadata 48 | shell: bash 49 | id: metadata 50 | run: | 51 | 52 | export GRAFANA_PLUGIN_ID=$(cat dist/plugin.json | jq -r .id) 53 | export GRAFANA_PLUGIN_VERSION=$(cat dist/plugin.json | jq -r .info.version) 54 | export GRAFANA_PLUGIN_TYPE=$(cat dist/plugin.json | jq -r .type) 55 | export GRAFANA_PLUGIN_ARTIFACT=${GRAFANA_PLUGIN_ID}-${GRAFANA_PLUGIN_VERSION}.zip 56 | export GRAFANA_PLUGIN_ARTIFACT_CHECKSUM=${GRAFANA_PLUGIN_ARTIFACT}.md5 57 | 58 | echo "plugin-id=${GRAFANA_PLUGIN_ID}" >> $GITHUB_OUTPUT 59 | echo "plugin-version=${GRAFANA_PLUGIN_VERSION}" >> $GITHUB_OUTPUT 60 | echo "plugin-type=${GRAFANA_PLUGIN_TYPE}" >> $GITHUB_OUTPUT 61 | echo "archive=${GRAFANA_PLUGIN_ARTIFACT}" >> $GITHUB_OUTPUT 62 | echo "archive-checksum=${GRAFANA_PLUGIN_ARTIFACT_CHECKSUM}" >> $GITHUB_OUTPUT 63 | 64 | echo "github-tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 65 | 66 | - name: Check package version 67 | shell: bash 68 | run: | 69 | if ! echo "v${{ steps.metadata.outputs.plugin-version }}" | grep -q "^${{ steps.metadata.outputs.github-tag }}.*$"; then 70 | printf "\033[0;31mPlugin version doesn't match tag name\033[0m\n" 71 | exit 1 72 | fi 73 | if: ${{ inputs.release == 'true' }} 74 | 75 | - name: Package plugin 76 | shell: bash 77 | id: package-plugin 78 | run: | 79 | mv dist ${{ steps.metadata.outputs.plugin-id }} 80 | zip ${{ steps.metadata.outputs.archive }} ${{ steps.metadata.outputs.plugin-id }} -r 81 | md5sum ${{ steps.metadata.outputs.archive }} > ${{ steps.metadata.outputs.archive-checksum }} 82 | echo "checksum=$(cat ./${{ steps.metadata.outputs.archive-checksum }} | cut -d' ' -f1)" >> $GITHUB_OUTPUT 83 | 84 | - name: Generate artifact attestation 85 | id: attestation 86 | uses: actions/attest-build-provenance@v2 87 | with: 88 | subject-path: '${{ steps.metadata.outputs.archive }}' 89 | 90 | - name: Upload plugin package 91 | uses: actions/upload-artifact@v4 92 | with: 93 | name: '${{ steps.metadata.outputs.archive }}' 94 | path: '${{ steps.metadata.outputs.archive }}' 95 | 96 | - name: Upload plugin checksum 97 | uses: actions/upload-artifact@v4 98 | with: 99 | name: '${{ steps.metadata.outputs.archive-checksum }}' 100 | path: '${{ steps.metadata.outputs.archive-checksum }}' 101 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | schedule: 9 | - cron: '38 0 * * *' # every day at randint(0, 60) minutes after midnight 10 | 11 | jobs: 12 | build: 13 | permissions: 14 | id-token: write 15 | contents: read 16 | attestations: write 17 | runs-on: ubuntu-latest 18 | outputs: 19 | signed-plugin-name: '${{ steps.build-signed.outputs.plugin_path }}' 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Setup Node.js environment 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: '20' 28 | cache: 'npm' 29 | 30 | - name: Install dependencies 31 | run: npm ci --prefer-offline 32 | 33 | - name: Run prettier 34 | run: npm run pretty 35 | 36 | - name: Check types 37 | run: npm run typecheck 38 | 39 | - name: Lint 40 | run: npm run lint 41 | 42 | - name: Unit tests 43 | run: npm run test:ci 44 | 45 | - name: Compatibility check 46 | run: npx @grafana/levitate@latest is-compatible --path src/module.ts --target @grafana/data,@grafana/ui,@grafana/runtime 47 | 48 | - name: clean up repo 49 | uses: actions/checkout@v4 50 | 51 | - name: Build frontend signed 52 | if: (github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == github.repository) 53 | id: build-signed 54 | uses: ./.github/build/ 55 | env: 56 | GRAFANA_ACCESS_POLICY_TOKEN: ${{ secrets.GRAFANA_API_KEY }} 57 | with: 58 | release: false 59 | signed: true 60 | 61 | validate-plugin: 62 | if: (github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == github.repository) 63 | runs-on: ubuntu-latest 64 | needs: build 65 | steps: 66 | - name: Download package from build job 67 | uses: actions/download-artifact@v4 68 | with: 69 | name: '${{ needs.build.outputs.signed-plugin-name }}' 70 | 71 | - name: Setup Go environment 72 | uses: actions/setup-go@v5 73 | with: 74 | go-version: '1.21' 75 | 76 | - name: Install validator 77 | run: | 78 | git clone https://github.com/grafana/plugin-validator 79 | pushd ./plugin-validator/pkg/cmd/plugincheck2 80 | go install 81 | popd 82 | 83 | - name: Validate plugin 84 | run: | 85 | plugincheck2 -config ./plugin-validator/config/publishing.yaml checkmk-cloud-datasource-*.zip 86 | 87 | e2e: 88 | runs-on: ubuntu-latest 89 | needs: build 90 | steps: 91 | - uses: actions/checkout@v4 92 | 93 | - name: Download package from build job 94 | uses: actions/download-artifact@v4 95 | with: 96 | name: '${{ needs.build.outputs.signed-plugin-name }}' 97 | 98 | - name: unpack and move plugin 99 | run: | 100 | unzip checkmk-cloud-datasource*.zip 101 | mv checkmk-cloud-datasource dist 102 | 103 | - name: run the e2e tests 104 | run: | 105 | docker compose rm -f 106 | docker compose up --exit-code-from=playwright --pull always 107 | working-directory: tests/ 108 | env: 109 | CI: true 110 | 111 | - name: upload playwright artifacts 112 | uses: actions/upload-artifact@v4 113 | if: always() 114 | with: 115 | name: playwright 116 | retention-days: 1 117 | path: tests/results 118 | if-no-files-found: ignore 119 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: 'CLA Assistant' 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened, closed, synchronize] 7 | 8 | # explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings 9 | permissions: 10 | actions: write 11 | contents: write 12 | pull-requests: write 13 | statuses: write 14 | 15 | jobs: 16 | CLAAssistant: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: 'CLA Assistant' 20 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA or my organization already has a signed CLA.') || github.event_name == 'pull_request_target' 21 | uses: contributor-assistant/github-action@v2.3.0 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | # the below token should have repo scope and must be manually added by you in the repository's secret 25 | # This token is required only if you have configured to store the signatures in a remote repository/organization 26 | PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_FOR_CLA }} 27 | with: 28 | path-to-signatures: 'signatures/version1/cla.json' 29 | path-to-document: 'https://github.com/tribe29/checkmk/blob/master/doc/cla/cla.md' # e.g. a CLA or a DCO document 30 | # branch should not be protected 31 | branch: 'main' 32 | allowlist: bot* # comma separated list of users who are allowed to submit without a CLA bot approval 33 | remote-organization-name: 'Checkmk' 34 | remote-repository-name: 'cla' 35 | custom-pr-sign-comment: 'I have read the CLA Document and I hereby sign the CLA or my organization already has a signed CLA.' 36 | 37 | # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken 38 | #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) 39 | #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' 40 | #signed-commit-message: 'For example: $contributorName has signed the CLA in $owner/$repo#$pullRequestNo' 41 | #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' 42 | #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' 43 | #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' 44 | #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) 45 | #use-dco-flag: true - If you are using DCO instead of CLA 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | jobs: 8 | release: 9 | permissions: 10 | id-token: write 11 | contents: write 12 | attestations: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Setup Node.js environment 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '20' 21 | cache: 'npm' 22 | 23 | - name: build signed 24 | id: build-signed 25 | uses: ./.github/build/ 26 | env: 27 | GRAFANA_ACCESS_POLICY_TOKEN: ${{ secrets.GRAFANA_API_KEY }} 28 | with: 29 | release: true 30 | signed: true 31 | 32 | - uses: actions/checkout@v4 33 | 34 | - name: download all action artifacs 35 | uses: actions/download-artifact@v4 36 | with: 37 | merge-multiple: true 38 | 39 | - name: Read changelog 40 | shell: bash 41 | run: | 42 | awk '/^## / {s++} s == 1 {print}' CHANGELOG.md > ${{ github.workspace }}-release_notes.md 43 | 44 | - name: create release 45 | uses: softprops/action-gh-release@v2 46 | with: 47 | body_path: ${{ github.workspace }}-release_notes.md 48 | draft: true 49 | files: | 50 | ${{ steps.build-signed.outputs.plugin_path }} 51 | ${{ steps.build-signed.outputs.plugin_checksum_path }} 52 | 53 | - uses: adelynx/github-actions-nodemailer@v1.0.3 54 | with: 55 | host: ${{ secrets.NOTIFICATION_SMTP_SERVER }} 56 | port: 465 57 | username: '${{ secrets.NOTIFICATION_SMTP_USER }}' 58 | password: '${{ secrets.NOTIFICATION_SMTP_PASSWORD }}' 59 | from: 'Grafana notifications <${{ secrets.NOTIFICATION_SMTP_USER }}>' 60 | to: '${{ secrets.NOTIFICATION_DESTINATION }}' 61 | subject: '✅ New release of Grafana data source plugin' 62 | 63 | body: | 64 |

A new release of the Grafana data source plugin has been published!

65 | -------------------------------------------------------------------------------- /.github/workflows/warn.yml: -------------------------------------------------------------------------------- 1 | name: Notify when daily build fails 2 | on: 3 | workflow_run: 4 | workflows: ['CI'] 5 | types: [completed] 6 | branches: [main] 7 | 8 | jobs: 9 | on-failure: 10 | runs-on: ubuntu-latest 11 | if: github.event.workflow_run.conclusion != 'success' && github.event.workflow_run.event == 'schedule' 12 | steps: 13 | - uses: actions/github-script@v6 14 | id: job-url 15 | with: 16 | script: | 17 | const { data } = await github.rest.actions.listJobsForWorkflowRunAttempt({ 18 | ...context.repo, 19 | run_id: context.runId, 20 | attempt_number: process.env.GITHUB_RUN_ATTEMPT 21 | }); 22 | return data.jobs[0].html_url 23 | result-encoding: string 24 | - name: Send mail 25 | uses: dawidd6/action-send-mail@v6 26 | with: 27 | server_address: ${{ secrets.NOTIFICATION_SMTP_SERVER }} 28 | server_port: 465 29 | secure: true 30 | ignore_cert: false 31 | username: '${{ secrets.NOTIFICATION_SMTP_USER }}' 32 | password: '${{ secrets.NOTIFICATION_SMTP_PASSWORD }}' 33 | from: 'Grafana notifications <${{ secrets.NOTIFICATION_SMTP_USER }}>' 34 | to: '${{ secrets.NOTIFICATION_DESTINATION }}' 35 | subject: '🔴 Grafana data source plugin nightly build failed' 36 | 37 | body: | 38 |

The latest Grafana's data source plugin nightly build has failed and needs manual intervention. Additional information:

39 |
    40 |
  • Process conclussion: ${{ github.event.workflow_run.conclusion }}
  • 41 |
  • More information: ${{ steps.job-url.outputs.result }}
  • 42 |
43 |

Thank you!

44 | 45 | #nodemailerlog: false 46 | #nodemailerdebug: false 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | .pnpm-debug.log* 7 | 8 | node_modules/ 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Compiled binary addons (https://nodejs.org/api/addons.html) 23 | dist/ 24 | artifacts/ 25 | work/ 26 | ci/ 27 | 28 | # e2e test directories 29 | tests/results/ 30 | playwright-report/ 31 | blob-report/ 32 | playwright/.cache/ 33 | playwright/.auth/ 34 | 35 | # Editor 36 | .idea 37 | .vscode 38 | 39 | # Local data 40 | .env 41 | local/ 42 | 43 | .eslintcache 44 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Prettier configuration provided by Grafana scaffolding 3 | ...require('./.config/.prettierrc.js'), 4 | plugins: ['@trivago/prettier-plugin-sort-imports'], 5 | importOrder: ['^components/(.*)$', '^[./]'], 6 | importOrderSeparation: true, 7 | importOrderSortSpecifiers: true, 8 | }; 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [//]: # 'The ci will use the first section starting with `##` as release notes.' 4 | 5 | ## 4.0.3 6 | - Updated dependencies 7 | 8 | ## 4.0.2 9 | - Dropped support for Grafana prior 10.4.18 10 | 11 | ## 4.0.1 12 | - Updated GitHub Actions 13 | 14 | ## 4.0.0 15 | - Removed signed plugin restrictions: The signed plugin works now with all Checkmk editions, including Raw Edition 16 | - Dropped support for unsigned plugin: Please migrate to the signed plugin 17 | - Dropped support for Checkmk prior to 2.2.0: Checkmk 2.1.0 reached EOL 2024-11-24 18 | - Updated documentation 19 | - Updated dependencies to latest versions 20 | - Bug fixing 21 | 22 | ## 3.4.1 23 | - Fix issue with user defined variables 24 | 25 | ## 3.4.0 26 | - Updated error message when Rest API cannot be found 27 | - Switched from yarn to npm 28 | - Updated dependencies to latest versions 29 | - Updated unit tests 30 | - Migrated end-to-end tests to Playwright 31 | - Updated documentation 32 | 33 | ## 3.3.0 34 | - Support for new autocomplete endpoints 35 | - Rebranding of Checkmk commercial products 36 | 37 | 38 | ## 3.2.2 39 | - Support for Checkmk Managed Edition 40 | - Align defaults for datasource settings 41 | **If you use checkmk raw edition and a provisioned datasource or created 42 | the datasource with an very old version of this plugin** you have to take 43 | manual action. See commit message [b6aa9b](https://github.com/Checkmk/grafana-checkmk-datasource/commit/b6aa99bff9dc4ab522d0b4eecd826dd694fcd606 "b6aa9b") 44 | - Clarify error message on 404 on REST-API request 45 | 46 | ## 3.2.1 47 | 48 | - Dropped support for Grafana prior 9.5.15 49 | 50 | ## 3.2.0 51 | 52 | - Dropped support for Grafana prior 9.0.0 53 | - At custom label field: 54 | - Filter values can be used as variables 55 | - Bugfix: Remove decimals from time periods as Checkmk does not support sub-second accuracy 56 | - Datasource configuration: 57 | - Mention feature degratadtion for older versions of Checkmk 58 | - Display an error message on edition mismatch 59 | - Update non-RAW editions name 60 | 61 | ## 3.1.1 62 | 63 | - update dependencies 64 | 65 | ## 3.1.0 66 | 67 | - added support for query variables 68 | - better error message for missing mandatory fields 69 | 70 | ## 3.0.1 71 | 72 | - We now build and ship a signed version of this plugin: 73 | - The signed plugin "Checkmk data source for Checkmk Cloud Edition" will 74 | only talk to the Cloud Edition of checkmk 75 | - the plugin id is `checkmk-cloud-datasource`, so dashboards created with 76 | the unsigned data source (id=`tribe-29-checkmk-datasource`) needs to be 77 | recreated. We will try to provide a script to automatically transfer 78 | existing dashboards in the near future. 79 | - The unsigned plugin (id=`tribe-29-checkmk-datasource`) will continue to 80 | work and will still be maintained. 81 | - Adapted the build process of the plugin to use `@grafana/create-plugin` 82 | - "tribe29 GmbH" is now "Checkmk GmbH" 83 | 84 | ## 3.0.0 85 | 86 | ### Highlights 87 | 88 | - Add support for REST API endpoints of Checkmk 2.2.0 89 | - Many UI improvements: 90 | - Clearer layout for both the RAW and CEE Query Editor 91 | - RAW filters are now displayed vertically aligned 92 | - The filter type of existing filters can no longer be changed, they have 93 | to be removed, and another filter can be added. 94 | - Errors in the Query Editor Fields are more prominently featured in the 95 | inputs themselves 96 | - Graphs in Grafana now show the same color as in Checkmk 97 | 98 | ### (Breaking) Changes 99 | 100 | * When using Checkmk < 2.2.0 you will have to choose the correct version in the 101 | data source settings, as this defaults to ">= 2.2" 102 | * Graph Types have been renamed, existing configuration is not affect by this. 103 | * "Template" Graphs are now called "Predefined Graphs" 104 | * "Metric" Graphs are now called "Single Metric" 105 | 106 | 107 | ## 2.0.3 108 | 109 | - Provide better error message on wrong authentication. 110 | 111 | ## 2.0.2 112 | 113 | - Removed `/dist` folder from git repository. That means that it's no longer 114 | possible to install this Grafana plugin by `git clone`. If you use `git pull` 115 | to update the plugin, you will have to change your deployment strategy of 116 | this plugin. Please refer to the [official documentation][1] 117 | - FIX: After changing a "Hostname Regex", the graph was not automatically 118 | updated, but Grafanas "Refresh dashboard" button on the upper right had 119 | to be clicked. Now the graph should update automatically. In order to 120 | limit the load on the checkmk server, the update is delayed by 500ms for 121 | text fields. 122 | This also affects: Hostname regex, Service Regex, Host is in Group, 123 | Service is in Group 124 | 125 | [1]: https://docs.checkmk.com/2.1.0/en/grafana.html 126 | 127 | ## 2.0.1 128 | 129 | - README.md already states that at least Grafana 8.0.0 is required, now the 130 | plugin also reflects that version requirement. 131 | - add missing logo file to dist folder 132 | 133 | ## 2.0.0 134 | 135 | Checkmk's Grafana connector underwent a complete rewrite. This plugin release 136 | accompanies checkmk 2.1, yet for testing purposes it will work with checkmk 137 | 2.0.0p20 138 | 139 | An update script is available to migrate the configuration from the previous 140 | connector to this new one. However, there are some backwards incompatible 141 | changes and not all features are conserved. 142 | 143 | We provide a Python script `utils/converter.py` in our [github project][github] which updates the Grafana 144 | SQLite database from the old connector setup to the new one. In that process it 145 | will go over all the dashboards and create a new version of them with the 146 | updated connector. **PLEASE BACKUP THIS FILE BEFORE UPDATING.** 147 | 148 | 1. Install and configure this new connector. Take note of the name you give it 149 | and take note of which name you gave the old connector. In this example we 150 | call them "Latest cmk connector" and "checkmk". 151 | 2. Stop your Grafana instance and backup the file `grafana.db` 152 | 3. Use the `converter.py` script, it has a `-h` option to remind you of the 153 | usage. To update from the previous datasource "checkmk" into this new 154 | connector "Latest cmk connector" on the `grafana.db` file, execute: 155 | 156 | ```BASH 157 | python3 converter.py -o "checkmk" -n "Latest cmk connector" -db grafana.db 158 | ``` 159 | 160 | If any of the two datasources is your default datasource, omit that option on 161 | the command. 162 | 163 | This script will go over all your dashboards, it might take some time because it 164 | also queries information from your checkmk site, and that communication takes 165 | time. 166 | 167 | 4. After the update completes start your Grafana server again. 168 | 169 | [github]: https://github.com/checkmk/grafana-checkmk-datasource/ 170 | 171 | ## 2.0.0b3 172 | 173 | - Update dependencies 174 | - Fix data source test on raw edition 175 | 176 | ## 2.0.0b2 177 | 178 | - Update dependencies 179 | - Filter METRIC\_ graph templates on CEE 180 | 181 | ## 2.0.0b1 182 | 183 | - Complete rewrite from scratch 184 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development Guide 2 | 3 | This document contains information on how to build and release the plugin. If 4 | you are looking to use the plugin please head over to [README.md](README.md). 5 | 6 | ## Building the plugin 7 | 8 | 1. Install dependencies 9 | 10 | ```BASH 11 | npm ci 12 | ``` 13 | 14 | 2. Build plugin in development mode or run in watch mode 15 | 16 | ```BASH 17 | npm run dev 18 | ``` 19 | 20 | 3. Build plugin in production mode 21 | 22 | ```BASH 23 | npm run build 24 | ``` 25 | 26 | ## Maintenance 27 | 28 | Code formatting 29 | 30 | ```BASH 31 | npm run pretty 32 | ``` 33 | 34 | Update dependencies 35 | 36 | https://grafana.com/developers/plugin-tools/migration-guides/update-create-plugin-versions 37 | 38 | ```BASH 39 | npx @grafana/create-plugin@latest update 40 | npm install 41 | npx npm-check-updates -i --format group 42 | npm run pretty:fix 43 | ``` 44 | 45 | ## Release 46 | 47 | - update dependencies 48 | - create pull request: 49 | - adapt version in package.json to `X.Y.Z` 50 | - add section in CHANGELOG.md: `## X.Y.Z` 51 | - merge pull request into main branch 52 | - tag commit in the main branch with `vX.Y.Z` 53 | - save release draft on GitHub 54 | 55 | ## E2E Tests 56 | 57 | There are serval ways to run the e2e tests. 58 | Make sure to have an up to date `dist/` folder using `npm run build`. 59 | 60 | ### No Interactivity use case (e.g. CI) 61 | 62 | ```BASH 63 | cd tests/ 64 | docker compose up --exit-code-from=playwright 65 | ``` 66 | 67 | This will run all tests without any further interaction necessary. 68 | 69 | 70 | ### Local development use case 71 | 72 | Please, note that you need to set serval environment variables as described for [No docker use case](#No-docker-use-case) 73 | 74 | ```BASH 75 | cd tests/ 76 | docker compose up -d checkmk grafana 77 | cd .. 78 | npm run e2e 79 | ``` 80 | 81 | You can run the tests on the console by runing 82 | 83 | ```BASH 84 | npm run e2e 85 | 86 | # Or you can run it in debug mode 87 | npm run e2e:debug 88 | ``` 89 | 90 | Or you can launch a nice interactive GUI to run and debug your E2E tests 91 | by running 92 | 93 | ```BASH 94 | npm run e2e:gui 95 | ``` 96 | 97 | See the official [docs](https://playwright.dev/docs/intro) for more information. 98 | 99 | ### No docker use case 100 | 101 | If you don't want to or can't use docker at all, make sure you have a Grafana and a CheckMK instance running somewhere. 102 | The Plugin you want to test needs to be installed in you Grafana instance. 103 | 104 | First you need to install the Playwright dependencies: 105 | ```BASH 106 | npx playwright install --with-deps 107 | ``` 108 | 109 | You also need to set serval environment variables. You can export them or define them in a .env file 110 | 111 | | Variable | Description | Example | 112 | | --------------------------- | ----------------------------------------------- | ---------------------------- | 113 | | CMK_SITE | Checkmk site name | cmk | 114 | | CMK_ADMIN | Administrator user name for Checkmk | cmkadmin | 115 | | CMK_AUTOMATION | Automation user name for Checkmk | automation | 116 | | CMK_PASSWORD | Password CMK_ADMIN and CMK_AUTOMATION | my_secret | 117 | | PLAYWRIGHT_TO_CHECKMK_URL | URL for connecting Playwright to Checkmk | http://127.0.0.1:12345/cmk/ | 118 | | PLAYWRIGHT_TO_GRAFANA_URL | URL for connecting Playwright to Grafana | http://127.0.0.1:3003/ | 119 | | GRAFANA_USER | User name for connecting to Grafana's Rest API | grafana_user | 120 | | GRAFANA_PASSWORD | Password for GRAFANA_USERNAME | my_other_secret | 121 | | GRAFANA_TO_CHECKMK_URL | URL for connecting Grafana to Checkmk | http://checkmk:5000/cmk/ | 122 | | GRAFANA_TO_CHECKMK_USER | Checkmk user name to access the Rest API | automation | 123 | | GRAFANA_TO_CHECKMK_PASSWORD | Password for GRAFANA_TO_CHECKMK_USER | my_secret | 124 | 125 | 126 | If everything is set up, just start cypress the usual way. 127 | 128 | ```BASH 129 | # Automatic run 130 | npm run e2e 131 | 132 | # or if you want to run it on debug mode 133 | npm run e2e:debug 134 | 135 | # or if you want to run it with the fancy web interface 136 | npm run e2e:gui 137 | ``` 138 | 139 | Please note that the test have side effects on your Grafana and CheckMK instance, 140 | such as creating a new automation user. 141 | 142 | ### Making sure you use the latest Grafana image 143 | Docker (compose) reuses already downloaded images as much as it can. So in order to get the latest Grafana image 144 | you need to remove the current image you have. The following snipped does a clean sweep. 145 | ```BASH 146 | docker compose down --rmi all 147 | ``` 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Checkmk GmbH 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Checkmk data source 2 | 3 | [![Grafana 10.4](https://img.shields.io/badge/Grafana-10.4-orange)](https://www.grafana.com) 4 | [![Grafana 11.6](https://img.shields.io/badge/Grafana-11.6-orange)](https://www.grafana.com) 5 | [![Checkmk data source for Checkmk Cloud & MSP](https://img.shields.io/badge/dynamic/json?color=blue&label=Checkmk%20for%20Cloud%20Edition&query=%24.version&url=https%3A%2F%2Fgrafana.com%2Fapi%2Fplugins%2Fcheckmk-cloud-datasource)](https://grafana.com/grafana/plugins/checkmk-cloud-datasource) 6 | [![CI](https://github.com/Checkmk/grafana-checkmk-datasource/actions/workflows/ci.yml/badge.svg?branch=main&event=schedule)](https://github.com/Checkmk/grafana-checkmk-datasource/actions/workflows/ci.yml?query=event%3Aschedule) 7 | 8 | 9 | ![Checkmk Grafana Data Source Plugin](https://github.com/checkmk/grafana-checkmk-datasource/raw/ebf24142922ccce5cc5649aa4809d1c19d55958f/grafana-checkmk-datasource.png) 10 | ## Introduction 11 | 12 | This [data source][2] plugin for [Grafana][1] allows to address Checkmk as source and to display individual metrics in Grafana. 13 | 14 | ## Requirements 15 | 16 | To make use of the plugin, you need to take care the correct versions are installed. You need to match both, the Grafana and the Checkmk version: 17 | 18 | - **Grafana 10.4.18 or higher** Current and previous major version of Grafana 19 | - **Checkmk 2.2.0 or higher** for the plugin available from [Github][8] or from [Grafana marketplace][6] 20 | 21 | ## Installing the plug-in 22 | 23 | The plug-in can be installed from the [Grafana marketplace][6] or by using the `grafana-cli`: 24 | 25 | ```bash 26 | grafana-cli plugins install checkmk-cloud-datasource 27 | ``` 28 | 29 | 30 | ## Official documentation 31 | 32 | For detailed information regarding the installation, configuration and usage, 33 | please consult the official [Checkmk User Guide][3]. 34 | 35 | ## Further information 36 | 37 | See [CHANGELOG.md][4] for information about updating from previous 38 | versions. 39 | 40 | See [DEVELOPMENT.md][5] for information about how to test, build and 41 | release this software. 42 | 43 | ## License 44 | 45 | This project is using the [Apache-2.0 license][7]. 46 | 47 | [1]: https://grafana.com/grafana/ 48 | [2]: https://grafana.com/docs/grafana/latest/datasources/ 49 | [3]: https://docs.checkmk.com/latest/en/grafana.html 50 | [4]: https://github.com/checkmk/grafana-checkmk-datasource/blob/main/CHANGELOG.md 51 | [5]: https://github.com/checkmk/grafana-checkmk-datasource/blob/main/DEVELOPMENT.md 52 | [6]: https://grafana.com/grafana/plugins/checkmk-cloud-datasource/ 53 | [7]: https://github.com/checkmk/grafana-checkmk-datasource/blob/main/LICENSE 54 | [8]: https://github.com/Checkmk/grafana-checkmk-datasource 55 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | grafana: 3 | container_name: 'checkmk-cloud-datasource' 4 | build: 5 | context: ./.config 6 | args: 7 | grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise} 8 | grafana_version: ${GRAFANA_VERSION:-10.4.0} 9 | development: ${DEVELOPMENT:-false} 10 | ports: 11 | - 3000:3000/tcp 12 | extra_hosts: 13 | - checkmk.local:host-gateway 14 | volumes: 15 | - ./dist:/var/lib/grafana/plugins/checkmk-cloud-datasource 16 | - ./provisioning:/etc/grafana/provisioning 17 | - .:/root/checkmk-cloud-datasource 18 | 19 | environment: 20 | NODE_ENV: development 21 | GF_LOG_FILTERS: plugin.checkmk-cloud-datasource:debug 22 | GF_LOG_LEVEL: debug 23 | GF_DATAPROXY_LOGGING: 1 24 | GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: checkmk-cloud-datasource 25 | 26 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | // Jest setup provided by Grafana scaffolding 2 | import './.config/jest-setup'; 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | // force timezone to UTC to allow tests to work regardless of local timezone 3 | // generally used by snapshots, but can affect specific tests 4 | process.env.TZ = 'UTC'; 5 | 6 | module.exports = { 7 | // Jest configuration provided by Grafana scaffolding 8 | ...require('./.config/jest.config'), 9 | //testMatch: ['/test/unit/**/*.{spec,test,jest}.{js,jsx,ts,tsx}'], 10 | testMatch: ['**/tests/unit/**/*.{spec,test,jest}.{js,jsx,ts,tsx}'], 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tribe-29-checkmk-datasource", 3 | "version": "4.0.3", 4 | "description": "Datasource for checkmk", 5 | "scripts": { 6 | "build": "webpack -c ./webpack.config.ts --env production", 7 | "build:cloud": "webpack -c ./webpack.config.ts --env production", 8 | "build:dev": "webpack -c ./webpack.config.ts --env development", 9 | "dev": "webpack -w -c ./webpack.config.ts --env development", 10 | "test": "jest --watch --onlyChanged", 11 | "test:ci": "jest --passWithNoTests --maxWorkers 4", 12 | "typecheck": "tsc --noEmit", 13 | "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .", 14 | "lint:fix": "npm run lint --fix", 15 | "pretty": "prettier -c '{**/*,*}.{js,ts,jsx,tsx,json,yml}'", 16 | "pretty:fix": "prettier -w '{**/*,*}.{js,ts,jsx,tsx,json,yml}'", 17 | "server": "docker-compose up --build", 18 | "start": "npm run watch", 19 | "sign": "npx --yes @grafana/sign-plugin@latest", 20 | "e2e": "playwright test", 21 | "e2e:ci": "playwright test", 22 | "e2e:debug": "playwright test --debug", 23 | "e2e:gui": "playwright test --ui", 24 | "e2e:show-trace": "playwright show-trace" 25 | }, 26 | "author": "Checkmk", 27 | "license": "Apache-2.0", 28 | "devDependencies": { 29 | "@babel/core": "^7.28.0", 30 | "@grafana/eslint-config": "^7.0.0", 31 | "@grafana/plugin-e2e": "^1.19.4", 32 | "@grafana/tsconfig": "^2.0.0", 33 | "@playwright/test": "^1.54.1", 34 | "@swc/core": "^1.13.2", 35 | "@swc/helpers": "^0.5.15", 36 | "@swc/jest": "^0.2.37", 37 | "@testing-library/jest-dom": "6.6.3", 38 | "@testing-library/react": "16.3.0", 39 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 40 | "@types/jest": "^29.5.14", 41 | "@types/lodash": "^4.17.20", 42 | "@types/node": "^22.15.19", 43 | "@types/react-router-dom": "^5.3.3", 44 | "copy-webpack-plugin": "^12.0.2", 45 | "css-loader": "^7.1.2", 46 | "dotenv": "^16.5.0", 47 | "eslint-plugin-deprecation": "^3.0.0", 48 | "eslint-webpack-plugin": "^4.2.0", 49 | "fork-ts-checker-webpack-plugin": "^9.1.0", 50 | "glob": "^11.0.3", 51 | "identity-obj-proxy": "3.0.0", 52 | "imports-loader": "^5.0.0", 53 | "jest": "^30.0.5", 54 | "jest-environment-jsdom": "^30.0.5", 55 | "prettier": "^3.6.2", 56 | "react-intersection-observer": "^9.16.0", 57 | "react-select-event": "^5.5.1", 58 | "replace-in-file-webpack-plugin": "^1.0.6", 59 | "sass": "1.89.2", 60 | "sass-loader": "16.0.5", 61 | "style-loader": "4.0.0", 62 | "swc-loader": "^0.2.6", 63 | "ts-node": "^10.9.2", 64 | "tsconfig-paths": "^4.2.0", 65 | "typescript": "5.8.3", 66 | "webpack": "^5.100.2", 67 | "webpack-cli": "^5.1.4", 68 | "webpack-livereload-plugin": "^3.0.2" 69 | }, 70 | "engines": { 71 | "node": ">=20" 72 | }, 73 | "dependencies": { 74 | "@emotion/css": "^11.13.5", 75 | "@grafana/data": "^12.0.2", 76 | "@grafana/runtime": "^11.6.2", 77 | "@grafana/schema": "^11.6.2", 78 | "@grafana/ui": "^11.6.2", 79 | "process": "^0.11.10", 80 | "react": "18.3.1", 81 | "react-dom": "18.3.1", 82 | "tslib": "2.8.1" 83 | }, 84 | "packageManager": "npm@10.9.0" 85 | } 86 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOptions } from '@grafana/plugin-e2e'; 2 | import { defineConfig } from '@playwright/test'; 3 | import { dirname } from 'path'; 4 | 5 | const pluginE2eAuth = `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`; 6 | 7 | export default defineConfig({ 8 | testDir: './tests/e2e/tests', 9 | outputDir: './tests/results', 10 | fullyParallel: false, 11 | forbidOnly: !!process.env.CI, 12 | retries: process.env.CI ? 3 : 0, 13 | expect: { 14 | timeout: 20_000, 15 | }, 16 | timeout: 60_000, 17 | workers: 1, 18 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 19 | reporter: 'list', 20 | use: { 21 | trace: 'retain-on-failure', 22 | video: 'retain-on-failure', 23 | screenshot: 'only-on-failure', 24 | viewport: { width: 1920, height: 1080 }, 25 | testIdAttribute: 'data-test-id', 26 | baseURL: process.env.PLAYWRIGHT_TO_GRAFANA_URL || 'http://localhost:3003', 27 | grafanaAPICredentials: { 28 | user: process.env.GRAFANA_USER || 'admin', 29 | password: process.env.GRAFANA_PASSWORD || 'admin', 30 | }, 31 | }, 32 | projects: [ 33 | { 34 | name: 'auth', 35 | testDir: pluginE2eAuth, 36 | testMatch: [/.*\.js/], 37 | }, 38 | { 39 | name: 'tests', 40 | testDir: './tests/e2e/tests', 41 | testMatch: ['**/*.test.ts'], 42 | use: { 43 | storageState: 'playwright/.auth/admin.json', 44 | }, 45 | dependencies: ['setup', 'auth'], 46 | }, 47 | { 48 | name: 'setup', 49 | testDir: './tests/e2e/tests', 50 | testMatch: 'global.setup.ts', 51 | teardown: 'tearDown', 52 | }, 53 | { 54 | name: 'tearDown', 55 | testDir: './tests/e2e/tests', 56 | testMatch: 'global.teardown.ts', 57 | }, 58 | ], 59 | }); 60 | -------------------------------------------------------------------------------- /src/DataSource.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataQueryRequest, 3 | DataQueryResponse, 4 | DataSourceApi, 5 | DataSourceInstanceSettings, 6 | MetricFindValue, 7 | TestDataSourceResponse, 8 | } from '@grafana/data'; 9 | import { FetchResponse } from '@grafana/runtime'; 10 | import { replaceVariables } from 'utils'; 11 | 12 | import { MetricFindQuery, RequestSpec } from './RequestSpec'; 13 | import RestApiBackend from './backend/rest'; 14 | import { Settings } from './settings'; 15 | import { CmkQuery, DataSourceOptions, Edition, ResponseDataAutocomplete, type WebApiResponse } from './types'; 16 | import { AutoCompleteParams } from './ui/autocomplete'; 17 | import { createCmkContext } from './utils'; 18 | 19 | export class DataSource extends DataSourceApi { 20 | restBackend: RestApiBackend; 21 | settings: Settings; 22 | 23 | constructor(private instanceSettings: DataSourceInstanceSettings) { 24 | super(instanceSettings); 25 | this.restBackend = new RestApiBackend(this); 26 | this.settings = new Settings(instanceSettings.jsonData); 27 | } 28 | 29 | async query(dataQueryRequest: DataQueryRequest): Promise { 30 | for (const target of dataQueryRequest.targets) { 31 | target.requestSpec = replaceVariables(target.requestSpec, dataQueryRequest.scopedVars); 32 | } 33 | return this.restBackend.query(dataQueryRequest); 34 | } 35 | 36 | async metricFindQuery(query: MetricFindQuery, options?: unknown): Promise { 37 | return await this.restBackend.metricFindQuery(query); 38 | } 39 | 40 | async testDatasource(): Promise { 41 | return this.restBackend.testDatasource(); 42 | } 43 | 44 | async autocompleterRequest( 45 | api_url: string, 46 | data: unknown 47 | ): Promise>> { 48 | return this.restBackend.autocompleterRequest(api_url, data); 49 | } 50 | 51 | async contextAutocomplete( 52 | ident: string, 53 | partialRequestSpec: Partial, 54 | prefix: string, 55 | params: Partial 56 | ): Promise> { 57 | const context = createCmkContext(replaceVariables(partialRequestSpec)); 58 | 59 | const response = await this.autocompleterRequest('ajax_vs_autocomplete.py', { 60 | ident, 61 | value: prefix, 62 | params: { 63 | ...params, 64 | context, 65 | }, 66 | }); 67 | return response.data.result.choices.map(([value, label]: [string, string]) => ({ 68 | value, 69 | label, 70 | isDisabled: value === null, 71 | })); 72 | } 73 | 74 | getUrl(): string | undefined { 75 | return this.instanceSettings.url; 76 | } 77 | 78 | getEdition(): Edition { 79 | return this.settings.edition; 80 | } 81 | 82 | getUsername(): string { 83 | return this.settings.username; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/RequestSpec.ts: -------------------------------------------------------------------------------- 1 | export interface NegatableOption { 2 | value?: string; 3 | negated: boolean; 4 | } 5 | 6 | export type ObjectType = 'host' | 'site' | 'service'; 7 | 8 | export interface MetricFindQuery { 9 | filter: Partial; 10 | objectType: ObjectType; 11 | } 12 | 13 | export interface RequestSpec { 14 | // TODO: we need to rename graph_type, as the graph_type differentiates between graph and metric. 15 | // TODO: we also need to rename graph, as this could contain a metric name. 16 | // my suggestion: entity_type and entity then it should be clear that they influence each other. 17 | graph_type: GraphType; 18 | 19 | aggregation: Aggregation; 20 | 21 | site: string | undefined; 22 | 23 | host_name: string | undefined; 24 | host_name_regex: NegatableOption | undefined; 25 | host_in_group: NegatableOption | undefined; 26 | host_labels: string[] | undefined; 27 | host_tags: [TagValue, TagValue, TagValue] | undefined; 28 | 29 | service: string | undefined; 30 | service_regex: NegatableOption | undefined; 31 | service_in_group: NegatableOption | undefined; 32 | 33 | graph: string | undefined; 34 | 35 | label: string | undefined; 36 | } 37 | 38 | // subset of RequestSpec used with the Filters Component 39 | export type FiltersRequestSpec = Pick< 40 | RequestSpec, 41 | | 'site' 42 | | 'host_name' 43 | | 'host_name_regex' 44 | | 'host_in_group' 45 | | 'host_labels' 46 | | 'host_tags' 47 | | 'service' 48 | | 'service_regex' 49 | | 'service_in_group' 50 | >; 51 | 52 | export interface TagValue { 53 | group?: string; 54 | tag?: string; 55 | operator?: 'is' | 'isnot'; 56 | } 57 | 58 | export type GraphType = 'single_metric' | 'predefined_graph'; 59 | 60 | export type Aggregation = 'off' | 'sum' | 'average' | 'minimum' | 'maximum'; 61 | 62 | export type PickByValue = Pick; 63 | 64 | export type RequestSpecStringKeys = keyof PickByValue; 65 | 66 | export type RequestSpecNegatableOptionKeys = keyof PickByValue; 67 | 68 | export const defaultRequestSpec: Partial = { 69 | aggregation: 'off', 70 | graph_type: 'predefined_graph', 71 | }; 72 | export type FilterEditorKeys = Exclude; 73 | -------------------------------------------------------------------------------- /src/api_utils.ts: -------------------------------------------------------------------------------- 1 | import { GraphType, NegatableOption, RequestSpec, TagValue } from './RequestSpec'; 2 | import { Context, Edition, Params } from './types'; 3 | import { aggregationToPresentation, createCmkContext, presentationToAggregation } from './utils'; 4 | 5 | export interface WebApiCurve { 6 | title: string; 7 | rrddata: Array<{ 8 | i: number; 9 | d: Record; 10 | }>; 11 | } 12 | export interface WebAPiGetGraphResult { 13 | start_time: number; 14 | end_time: number; 15 | step: number; 16 | curves: WebApiCurve[]; 17 | } 18 | 19 | export interface WebApiResponse { 20 | result_code: number; 21 | result: Result; 22 | severity: string | undefined; 23 | } 24 | 25 | function transform_negated( 26 | context_property: Record | undefined, 27 | key_name: string 28 | ): NegatableOption | undefined { 29 | if (context_property === undefined) { 30 | return undefined; 31 | } 32 | return { 33 | value: context_property[key_name] || '', 34 | negated: context_property[`neg_${key_name}`] === 'on', 35 | }; 36 | } 37 | 38 | function graphModeToGraphType(graph_mode: 'template' | 'metric'): GraphType { 39 | if (graph_mode === 'template') { 40 | return 'predefined_graph'; 41 | } 42 | if (graph_mode === 'metric') { 43 | return 'single_metric'; 44 | } 45 | throw Error(`graph_mode ${graph_mode} is not known`); 46 | } 47 | 48 | export function requestSpecFromLegacy(context: Context, params: Params): Partial { 49 | const rs: Partial = {}; 50 | if (context.host !== undefined) { 51 | rs.host_name = context.host.host; 52 | } 53 | 54 | if (context.siteopt !== undefined) { 55 | rs.site = context.siteopt.site; 56 | } 57 | 58 | rs.host_name_regex = transform_negated(context.hostregex, 'host_regex'); 59 | 60 | if (context.service !== undefined) { 61 | rs.service = context.service.service; 62 | } 63 | 64 | rs.service_regex = transform_negated(context.serviceregex, 'service_regex'); 65 | 66 | rs.host_in_group = transform_negated(context.opthostgroup, 'opthost_group'); 67 | rs.service_in_group = transform_negated(context.optservicegroup, 'optservice_group'); 68 | 69 | if (context.host_labels !== undefined) { 70 | const host_labels = JSON.parse(context.host_labels.host_label); 71 | rs.host_labels = host_labels.map((v: { value: string }) => v['value']); 72 | } 73 | 74 | if (context.host_tags !== undefined) { 75 | const result: [TagValue, TagValue, TagValue] = [{}, {}, {}]; 76 | const numbers: Array<0 | 1 | 2> = [0, 1, 2]; 77 | for (const i of numbers) { 78 | const group = context.host_tags[`host_tag_${i}_grp`]; 79 | const operator = context.host_tags[`host_tag_${i}_op`]; 80 | const tag = context.host_tags[`host_tag_${i}_val`]; 81 | if (group !== undefined && operator !== undefined && tag !== undefined) { 82 | result[i] = { group, operator, tag }; 83 | } 84 | } 85 | rs.host_tags = result; 86 | } 87 | 88 | rs.graph_type = graphModeToGraphType(params.graphMode); 89 | if (rs.graph_type === 'single_metric') { 90 | rs.graph = params.graph_name; 91 | } else { 92 | rs.graph = params.graph_name; // TODO: make this happen! 93 | } 94 | rs.aggregation = presentationToAggregation(params.presentation); 95 | return rs; 96 | } 97 | 98 | export function createWebApiRequestSpecification( 99 | requestSpec: Partial, 100 | edition: Edition 101 | ): [string, Record] { 102 | if (edition === 'RAW') { 103 | const specification: Record = {}; 104 | if (requestSpec.graph_type === 'single_metric') { 105 | specification.graph_id = 'METRIC_' + requestSpec.graph; 106 | } else { 107 | specification.graph_name = requestSpec.graph; 108 | } 109 | return [ 110 | 'template', 111 | { 112 | site: requestSpec.site, 113 | host_name: requestSpec.host_name, 114 | service_description: requestSpec.service, 115 | ...specification, 116 | }, 117 | ]; 118 | } 119 | const context = createCmkContext(requestSpec); 120 | let graph_template: string | undefined = undefined; 121 | if (requestSpec.graph && requestSpec.graph !== '') { 122 | if (requestSpec.graph_type === 'single_metric') { 123 | graph_template = 'METRIC_' + requestSpec.graph; 124 | } else { 125 | graph_template = requestSpec.graph; 126 | } 127 | } 128 | 129 | if (requestSpec.aggregation === undefined) { 130 | throw new Error('web api: aggregation not defined!'); 131 | } 132 | 133 | return [ 134 | 'combined', 135 | { 136 | context: context, 137 | datasource: 'services', 138 | single_infos: ['host'], 139 | graph_template: graph_template, 140 | presentation: aggregationToPresentation(requestSpec.aggregation), 141 | }, 142 | ]; 143 | } 144 | 145 | export const buildUrlWithParams = (url: string, params: Record): string => 146 | url + '?' + new URLSearchParams(params).toString(); 147 | export const buildRequestBody = (data: unknown): string => `request=${JSON.stringify(data)}`; 148 | 149 | export function createWebApiRequestBody(context: [string, Record], timeRange: number[]) { 150 | return { 151 | specification: context, 152 | data_range: { time_range: timeRange }, 153 | }; 154 | } 155 | -------------------------------------------------------------------------------- /src/backend/types.ts: -------------------------------------------------------------------------------- 1 | import { DataQueryRequest, DataQueryResponse, MetricFindValue, TestDataSourceResponse } from '@grafana/data'; 2 | 3 | import { MetricFindQuery } from '../RequestSpec'; 4 | import { CmkQuery, Edition } from '../types'; 5 | 6 | export interface Backend { 7 | query: (options: DataQueryRequest) => Promise; 8 | testDatasource: () => Promise; 9 | metricFindQuery: (query: MetricFindQuery) => Promise; 10 | listSites: () => Promise; 11 | } 12 | 13 | export interface DatasourceOptions { 14 | getEdition: () => Edition; 15 | getUrl: () => string | undefined; 16 | getUsername(): string; 17 | } 18 | -------------------------------------------------------------------------------- /src/backend/validate.ts: -------------------------------------------------------------------------------- 1 | import { RequestSpec } from '../RequestSpec'; 2 | import { Edition } from '../types'; 3 | import { labelForRequestSpecKey } from '../ui/utils'; 4 | 5 | // 'graph_type' and 'aggregation' should always have a default value 6 | type PotentiallyRequiredKeys = 'site' | 'service' | 'host_name' | 'graph'; 7 | 8 | const missingRequiredFields = (rq: Partial, edition: Edition): string[] => { 9 | const result: PotentiallyRequiredKeys[] = []; 10 | if (rq.graph === undefined || rq.graph === '') { 11 | result.push('graph'); 12 | } 13 | 14 | if (edition === 'RAW') { 15 | if (rq.site === undefined) { 16 | result.push('site'); 17 | } 18 | if (rq.service === undefined || rq.service === '') { 19 | result.push('service'); 20 | } 21 | if (rq.host_name === undefined || rq.service === '') { 22 | result.push('host_name'); 23 | } 24 | } 25 | return result.sort().map((value) => labelForRequestSpecKey(value, rq)); 26 | }; 27 | 28 | export const validateRequestSpec = (rq: Partial | undefined, edition: Edition): void => { 29 | if (rq === undefined) { 30 | rq = {}; 31 | } 32 | 33 | const missingFields = missingRequiredFields(rq, edition); 34 | if (missingFields.length === 0) { 35 | return; 36 | } 37 | throw new Error(`Please specify a value for the following fields: ${missingFields.join(', ')}`); 38 | }; 39 | -------------------------------------------------------------------------------- /src/img/checkmk_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Checkmk/grafana-checkmk-datasource/5baf34e66a585db7befb2e448357d697ea2153ee/src/img/screenshot.png -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { DataSourcePlugin } from '@grafana/data'; 2 | 3 | import { DataSource } from './DataSource'; 4 | import { CmkQuery, DataSourceOptions } from './types'; 5 | import { ConfigEditor } from './ui/ConfigEditor'; 6 | import { QueryEditor } from './ui/QueryEditor'; 7 | import { VariableQueryEditor } from './ui/VariableQueryEditor'; 8 | 9 | export const plugin = new DataSourcePlugin(DataSource) 10 | .setConfigEditor(ConfigEditor) 11 | .setVariableQueryEditor(VariableQueryEditor) 12 | .setQueryEditor(QueryEditor); 13 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json", 3 | "type": "datasource", 4 | "name": "Checkmk data source", 5 | "id": "checkmk-cloud-datasource", 6 | "metrics": true, 7 | "info": { 8 | "description": "Checkmk data source for Checkmk 2.2.0 or higher", 9 | "author": { 10 | "name": "Checkmk GmbH", 11 | "url": "https://github.com/Checkmk/" 12 | }, 13 | "keywords": ["checkmk"], 14 | "logos": { 15 | "small": "img/checkmk_logo.svg", 16 | "large": "img/checkmk_logo.svg" 17 | }, 18 | "links": [ 19 | { 20 | "name": "Documentation", 21 | "url": "https://docs.checkmk.com/latest/en/grafana.html" 22 | }, 23 | { 24 | "name": "GitHub", 25 | "url": "https://github.com/Checkmk/grafana-checkmk-datasource" 26 | }, 27 | { 28 | "name": "Checkmk", 29 | "url": "https://Checkmk.com" 30 | } 31 | ], 32 | "screenshots": [ 33 | { 34 | "name": "Checkmk metrics shown in Grafana", 35 | "path": "img/screenshot.png" 36 | } 37 | ], 38 | "version": "%VERSION%", 39 | "updated": "%TODAY%" 40 | }, 41 | "dependencies": { 42 | "grafanaDependency": ">=10.4.18", 43 | "plugins": [] 44 | }, 45 | "routes": [ 46 | { 47 | "path": "rest", 48 | "url": "{{ .JsonData.url }}", 49 | "headers": [ 50 | { 51 | "name": "Authorization", 52 | "content": "Bearer {{ .JsonData.username }} {{ .SecureJsonData.secret }}" 53 | } 54 | ] 55 | }, 56 | { 57 | "path": "cmk", 58 | "url": "{{ .JsonData.url }}", 59 | "urlParams": [ 60 | { 61 | "name": "_username", 62 | "content": "{{ .JsonData.username }}" 63 | }, 64 | { 65 | "name": "_secret", 66 | "content": "{{ .SecureJsonData.secret }}" 67 | }, 68 | { 69 | "name": "output_format", 70 | "content": "json" 71 | } 72 | ] 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceOptions, Edition } from './types'; 2 | 3 | export class Settings { 4 | protected settings: DataSourceOptions; 5 | 6 | constructor(settings: DataSourceOptions) { 7 | this.settings = settings; 8 | } 9 | 10 | get edition(): Edition { 11 | // cloud instances of this plugin don't save the edition and use this default 12 | return this.settings.edition ?? 'CEE'; 13 | } 14 | 15 | get url(): string | undefined { 16 | return this.settings.url; 17 | } 18 | 19 | get username(): string { 20 | return this.settings.username ?? ''; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceJsonData } from '@grafana/data'; 2 | import { DataQuery } from '@grafana/schema'; 3 | 4 | import { RequestSpec, defaultRequestSpec } from './RequestSpec'; 5 | 6 | export type ContextHTTPVars = Record; 7 | 8 | // TODO: should be moved to webapi types 9 | type Negative = undefined | 'on' | ''; 10 | 11 | // TODO: should be moved to webapi types 12 | export interface Context { 13 | host?: { 14 | host: string; 15 | }; 16 | siteopt?: { 17 | site: string; 18 | }; 19 | hostregex?: { host_regex: string; neg_host_regex?: Negative }; 20 | service?: { service: string }; 21 | serviceregex?: { neg_service_regex?: Negative; service_regex: string }; 22 | host_labels?: Record; 23 | opthostgroup?: { opthost_group: string; neg_opthost_group?: Negative }; 24 | optservicegroup?: { optservice_group: string; neg_optservice_group?: Negative }; 25 | host_tags?: { 26 | host_tag_0_grp?: string; 27 | host_tag_0_op?: 'is' | 'isnot'; 28 | host_tag_0_val?: string; 29 | host_tag_1_grp?: string; 30 | host_tag_1_op?: 'is' | 'isnot'; 31 | host_tag_1_val?: string; 32 | host_tag_2_grp?: string; 33 | host_tag_2_op?: 'is' | 'isnot'; 34 | host_tag_2_val?: string; 35 | }; 36 | } 37 | 38 | // TODO: should be moved to webapi types 39 | export interface Params { 40 | graphMode: 'metric' | 'template'; 41 | graph_name: string; 42 | presentation: 'lines' | 'sum' | 'average' | 'min' | 'max'; 43 | selections: unknown; 44 | } 45 | 46 | export interface CmkQuery extends DataQuery { 47 | requestSpec: Partial; 48 | /** 49 | * @deprecated legacy interface context should not be used, use requestSpec 50 | */ 51 | context?: Context; 52 | /** 53 | * @deprecated legacy interface params should not be used, use requestSpec 54 | */ 55 | params?: Params; 56 | } 57 | 58 | export const defaultQuery: Partial = { 59 | requestSpec: defaultRequestSpec, 60 | }; 61 | 62 | export type Edition = 'CEE' | 'RAW'; 63 | export type Backend = 'web' | 'rest'; 64 | 65 | export interface DataSourceOptions extends DataSourceJsonData { 66 | url?: string; 67 | username?: string; 68 | edition?: Edition; 69 | backend?: Backend; 70 | } 71 | 72 | /** 73 | * Value that is used in the backend, but never sent over HTTP to the frontend 74 | */ 75 | export interface SecureJsonData { 76 | secret?: string; 77 | } 78 | 79 | export interface ResponseDataAutocomplete { 80 | choices: Array<[string, string]>; 81 | } 82 | 83 | export enum LabelVariableNames { 84 | ORIGINAL = '$label', 85 | SITE = '$filter_site', 86 | HOSTNAME = '$filter_host_name', 87 | HOST_IN_GROUP = '$filter_host_in_group', 88 | SERVICE = '$filter_service', 89 | SERVICE_IN_GROUP = '$filter_service_in_group', 90 | } 91 | 92 | export interface WebApiResponse { 93 | result_code: number; 94 | result: Result; 95 | severity: string | undefined; 96 | } 97 | -------------------------------------------------------------------------------- /src/ui/ConfigEditor.tsx: -------------------------------------------------------------------------------- 1 | import { DataSourcePluginOptionsEditorProps, SelectableValue } from '@grafana/data'; 2 | import { FieldSet, InlineField, LegacyForms, Select } from '@grafana/ui'; 3 | import React, { ChangeEvent, useCallback } from 'react'; 4 | 5 | import { Settings } from '../settings'; 6 | import { DataSourceOptions, Edition, SecureJsonData } from '../types'; 7 | 8 | const { SecretFormField, FormField } = LegacyForms; 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 11 | interface Props extends DataSourcePluginOptionsEditorProps {} 12 | 13 | interface EditionOption { 14 | value: Edition; 15 | label: string; 16 | } 17 | 18 | const cmkEditions: EditionOption[] = [ 19 | { value: 'CEE', label: 'Commercial editions' }, 20 | { value: 'RAW', label: 'Raw Edition' }, 21 | ]; 22 | 23 | export const ConfigEditor = (props: Props) => { 24 | const onUrlChange = useCallback( 25 | (event: ChangeEvent): void => { 26 | const { onOptionsChange, options } = props; 27 | const jsonData = { 28 | ...options.jsonData, 29 | url: event.target.value, 30 | }; 31 | onOptionsChange({ ...options, jsonData }); 32 | }, 33 | [props] 34 | ); 35 | 36 | const onEditionChange = useCallback( 37 | ({ value }: SelectableValue): void => { 38 | const { onOptionsChange, options } = props; 39 | const jsonData = { 40 | ...options.jsonData, 41 | edition: value, 42 | }; 43 | onOptionsChange({ ...options, jsonData }); 44 | }, 45 | [props] 46 | ); 47 | 48 | const onUsernameChange = useCallback( 49 | (event: ChangeEvent): void => { 50 | const { onOptionsChange, options } = props; 51 | const jsonData = { 52 | ...options.jsonData, 53 | username: event.target.value, 54 | }; 55 | onOptionsChange({ ...options, jsonData }); 56 | }, 57 | [props] 58 | ); 59 | 60 | // Secure field (only sent to the backend) 61 | const onSecretChange = useCallback( 62 | (event: ChangeEvent): void => { 63 | const { onOptionsChange, options } = props; 64 | onOptionsChange({ 65 | ...options, 66 | secureJsonData: { 67 | secret: event.target.value, 68 | }, 69 | }); 70 | }, 71 | [props] 72 | ); 73 | 74 | const onResetSecret = useCallback((): void => { 75 | const { onOptionsChange, options } = props; 76 | onOptionsChange({ 77 | ...options, 78 | secureJsonFields: { 79 | ...options.secureJsonFields, 80 | secret: false, 81 | }, 82 | secureJsonData: { 83 | ...options.secureJsonData, 84 | secret: '', 85 | }, 86 | }); 87 | }, [props]); 88 | 89 | const { options } = props; 90 | const { jsonData, secureJsonFields } = options; 91 | const secureJsonData = options.secureJsonData || {}; 92 | const settings = new Settings(jsonData); 93 | 94 | return ( 95 | <> 96 |
97 |
98 | 107 |
108 | 109 |