Compare commits

...

25 Commits

Author SHA1 Message Date
Yue Yang 91a1f3fa41 Update README.md
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2024-03-25 16:36:50 +08:00
Yue Yang ce64162aff docs: add authentication section
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2024-03-25 15:44:13 +08:00
Yue Yang b12d852975 Update CHANGELOG.md
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2024-01-10 10:44:34 +08:00
Yue Yang 55cd414baa style: better annotaions display
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2024-01-08 14:46:26 +08:00
Yue Yang c708e2760b feat: support multi-value variables
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2024-01-08 14:16:58 +08:00
Yue Yang f0c1e6c014 chore: re-add policy_token
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2023-12-22 10:39:58 +08:00
Yue Yang 5b31696bb7 fix(security): upgrade underscore to 1.13.6
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2023-12-21 11:10:47 +08:00
Yue Yang 604a2b42a8 Create REVIEW.md
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2023-12-19 13:58:21 +08:00
Yue Yang efdcb7015f fix: comment policy_token for review
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2023-12-19 11:38:21 +08:00
Yue Yang 9f60dca6b4 chore: prepare v3 release
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2023-12-19 11:15:07 +08:00
Yue Yang 751aadcf89 chore(readme): update version to 3.0.0
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2023-12-18 18:31:13 +08:00
Yue Yang 19570eb209 chore: add provisioning files
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2023-12-18 18:25:22 +08:00
Yue Yang 53d5eb9236
refactor!: deprecate Angular support (#59)
* chore: init

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>

* refactor: rewrite query

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>

* feat: allow custom/clear value

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>

* refactor: rewrite variables

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>

* chore: remove semicolons

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>

* chore: add default annotations

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>

* refactor: reimplement annotations

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>

* chore: update plugin info

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>

---------

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2023-12-18 15:46:22 +08:00
Yue Yang c7192de2bd chore: update bundle dir
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2022-08-11 11:13:10 +08:00
Yue Yang b9e9aad715 v2.2.3
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2022-08-07 00:50:50 +08:00
Yue Yang 1eb3ef25fc Update annotations.png 2022-08-07 00:46:49 +08:00
Yue Yang 86404dbd65 fix: remove useless operations
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2022-08-03 00:22:21 +08:00
Yue Yang 0691befc9c fix: Datasource to Data Source
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2022-08-03 00:21:45 +08:00
Yue Yang ebcbb6d580 chore: uncomment `yarn sign`
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2022-08-02 19:22:33 +08:00
Yue Yang c56081a00f v2.2.2
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2022-07-31 21:41:38 +08:00
Yue Yang d7ec01506a feat: ready for submission!
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2022-07-31 21:38:46 +08:00
Yue Yang 874a4601ce v2.2.1
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2022-07-28 22:38:50 +08:00
Yue Yang 46fb45d1e0 fix: reset kind field to input in annotations
This allows you to use variables in the kind field, such as $kind.

Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2022-07-28 22:29:48 +08:00
Yue Yang 6c2e2c2b94 fix: clone query to avoid mutating it
Signed-off-by: Yue Yang <g1enyy0ung@gmail.com>
2022-07-28 22:14:35 +08:00
dependabot[bot] e2c8bb38c1
build(deps): bump terser from 4.8.0 to 4.8.1 (#48)
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-28 21:43:08 +08:00
58 changed files with 12714 additions and 15197 deletions

3
.config/.cprc.json Normal file
View File

@ -0,0 +1,3 @@
{
"version": "2.10.1"
}

25
.config/.eslintrc Normal file
View File

@ -0,0 +1,25 @@
/*
* ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-eslint-config
*/
{
"extends": ["@grafana/eslint-config"],
"root": true,
"rules": {
"react/prop-types": "off"
},
"overrides": [
{
"plugins": ["deprecation"],
"files": ["src/**/*.{ts,tsx}"],
"rules": {
"deprecation/deprecation": "warn"
},
"parserOptions": {
"project": "./tsconfig.json"
}
}
]
}

16
.config/.prettierrc.js Normal file
View File

@ -0,0 +1,16 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in .config/README.md
*/
module.exports = {
endOfLine: 'auto',
printWidth: 120,
trailingComma: 'es5',
semi: true,
jsxSingleQuote: false,
singleQuote: true,
useTabs: false,
tabWidth: 2,
};

16
.config/Dockerfile Normal file
View File

@ -0,0 +1,16 @@
ARG grafana_version=latest
ARG grafana_image=grafana-enterprise
FROM grafana/${grafana_image}:${grafana_version}
# Make it as simple as possible to access the grafana instance for development purposes
# Do NOT enable these settings in a public facing / production grafana instance
ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin"
ENV GF_AUTH_ANONYMOUS_ENABLED "true"
ENV GF_AUTH_BASIC_ENABLED "false"
# Set development mode so plugins can be loaded without the need to sign
ENV GF_DEFAULT_APP_MODE "development"
# Inject livereload script into grafana index.html
USER root
RUN sed -i 's/<\/body><\/html>/<script src=\"http:\/\/localhost:35729\/livereload.js\"><\/script><\/body><\/html>/g' /usr/share/grafana/public/views/index.html

164
.config/README.md Normal file
View File

@ -0,0 +1,164 @@
# Default build configuration by Grafana
**This is an auto-generated directory and is not intended to be changed! ⚠️**
The `.config/` directory holds basic configuration for the different tools
that are used to develop, test and build the project. In order to make it updates easier we ask you to
not edit files in this folder to extend configuration.
## How to extend the basic configs?
Bear in mind that you are doing it at your own risk, and that extending any of the basic configuration can lead
to issues around working with the project.
### Extending the ESLint config
Edit the `.eslintrc` file in the project root in order to extend the ESLint configuration.
**Example:**
```json
{
"extends": "./.config/.eslintrc",
"rules": {
"react/prop-types": "off"
}
}
```
---
### Extending the Prettier config
Edit the `.prettierrc.js` file in the project root in order to extend the Prettier configuration.
**Example:**
```javascript
module.exports = {
// Prettier configuration provided by Grafana scaffolding
...require('./.config/.prettierrc.js'),
semi: false,
};
```
---
### Extending the Jest config
There are two configuration in the project root that belong to Jest: `jest-setup.js` and `jest.config.js`.
**`jest-setup.js`:** A file that is run before each test file in the suite is executed. We are using it to
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))
**`jest.config.js`:** The main Jest configuration file that extends the Grafana recommended setup. ([link to Jest docs](https://jestjs.io/docs/configuration))
#### ESM errors with Jest
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:
```javascript
process.env.TZ = 'UTC';
const { grafanaESModules, nodeModulesToTransform } = require('./config/jest/utils');
module.exports = {
// Jest configuration provided by Grafana
...require('./.config/jest.config'),
// Inform jest to only transform specific node_module packages.
transformIgnorePatterns: [nodeModulesToTransform([...grafanaESModules, 'packageName'])],
};
```
---
### Extending the TypeScript config
Edit the `tsconfig.json` file in the project root in order to extend the TypeScript configuration.
**Example:**
```json
{
"extends": "./.config/tsconfig.json",
"compilerOptions": {
"preserveConstEnums": true
}
}
```
---
### Extending the Webpack config
Follow these steps to extend the basic Webpack configuration that lives under `.config/`:
#### 1. Create a new Webpack configuration file
Create a new config file that is going to extend the basic one provided by Grafana.
It can live in the project root, e.g. `webpack.config.ts`.
#### 2. Merge the basic config provided by Grafana and your custom setup
We are going to use [`webpack-merge`](https://github.com/survivejs/webpack-merge) for this.
```typescript
// webpack.config.ts
import type { Configuration } from 'webpack';
import { merge } from 'webpack-merge';
import grafanaConfig from './.config/webpack/webpack.config';
const config = async (env): Promise<Configuration> => {
const baseConfig = await grafanaConfig(env);
return merge(baseConfig, {
// Add custom config here...
output: {
asyncChunks: true,
},
});
};
export default config;
```
#### 3. Update the `package.json` to use the new Webpack config
We need to update the `scripts` in the `package.json` to use the extended Webpack configuration.
**Update for `build`:**
```diff
-"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
+"build": "webpack -c ./webpack.config.ts --env production",
```
**Update for `dev`:**
```diff
-"dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development",
+"dev": "webpack -w -c ./webpack.config.ts --env development",
```
### Configure grafana image to use when running docker
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`.
**Example:**
```yaml
version: '3.7'
services:
grafana:
container_name: 'myorg-basic-app'
build:
context: ./.config
args:
grafana_version: ${GRAFANA_VERSION:-9.1.2}
grafana_image: ${GRAFANA_IMAGE:-grafana}
```
In this example, we assign the environment variable `GRAFANA_IMAGE` to the build arg `grafana_image` with a default value of `grafana`. This will allow you to set the value while running the docker-compose commands, which might be convenient in some scenarios.
---

25
.config/jest-setup.js Normal file
View File

@ -0,0 +1,25 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config
*/
import '@testing-library/jest-dom';
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(global, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
HTMLCanvasElement.prototype.getContext = () => {};

43
.config/jest.config.js Normal file
View File

@ -0,0 +1,43 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config
*/
const path = require('path');
const { grafanaESModules, nodeModulesToTransform } = require('./jest/utils');
module.exports = {
moduleNameMapper: {
'\\.(css|scss|sass)$': 'identity-obj-proxy',
'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'),
},
modulePaths: ['<rootDir>/src'],
setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],
testEnvironment: 'jest-environment-jsdom',
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}',
],
transform: {
'^.+\\.(t|j)sx?$': [
'@swc/jest',
{
sourceMaps: 'inline',
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
decorators: false,
dynamicImport: true,
},
},
},
],
},
// Jest will throw `Cannot use import statement outside module` if it tries to load an
// ES module without it being transformed first. ./config/README.md#esm-errors-with-jest
transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)],
};

View File

@ -0,0 +1,25 @@
// Due to the grafana/ui Icon component making fetch requests to
// `/public/img/icon/<icon_name>.svg` we need to mock react-inlinesvg to prevent
// the failed fetch requests from displaying errors in console.
import React from 'react';
type Callback = (...args: any[]) => void;
export interface StorageItem {
content: string;
queue: Callback[];
status: string;
}
export const cacheStore: { [key: string]: StorageItem } = Object.create(null);
const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/;
const InlineSVG = ({ src }: { src: string }) => {
// testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`)
const testId = src.replace(SVG_FILE_NAME_REGEX, '$2');
return <svg xmlns="http://www.w3.org/2000/svg" data-testid={testId} viewBox="0 0 24 24" />;
};
export default InlineSVG;

31
.config/jest/utils.js Normal file
View File

@ -0,0 +1,31 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in .config/README.md
*/
/*
* This utility function is useful in combination with jest `transformIgnorePatterns` config
* to transform specific packages (e.g.ES modules) in a projects node_modules folder.
*/
const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNames.join('|')})\/.*)`;
// Array of known nested grafana package dependencies that only bundle an ESM version
const grafanaESModules = [
'.pnpm', // Support using pnpm symlinked packages
'@grafana/schema',
'd3',
'd3-color',
'd3-force',
'd3-interpolate',
'd3-scale-chromatic',
'ol',
'react-colorful',
'rxjs',
'uuid',
];
module.exports = {
nodeModulesToTransform,
grafanaESModules,
};

26
.config/tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-typescript-config
*/
{
"compilerOptions": {
"alwaysStrict": true,
"declaration": false,
"rootDir": "../src",
"baseUrl": "../src",
"typeRoots": ["../node_modules/@types"],
"resolveJsonModule": true
},
"ts-node": {
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"esModuleInterop": true
},
"transpileOnly": true
},
"include": ["../src", "./types"],
"extends": "@grafana/tsconfig"
}

37
.config/types/custom.d.ts vendored Normal file
View File

@ -0,0 +1,37 @@
// Image declarations
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}
declare module '*.svg' {
const content: string;
export default content;
}
// Font declarations
declare module '*.woff';
declare module '*.woff2';
declare module '*.eot';
declare module '*.ttf';
declare module '*.otf';

View File

@ -0,0 +1,2 @@
export const SOURCE_DIR = 'src';
export const DIST_DIR = 'dist';

58
.config/webpack/utils.ts Normal file
View File

@ -0,0 +1,58 @@
import fs from 'fs';
import process from 'process';
import os from 'os';
import path from 'path';
import { glob } from 'glob';
import { SOURCE_DIR } from './constants';
export function isWSL() {
if (process.platform !== 'linux') {
return false;
}
if (os.release().toLowerCase().includes('microsoft')) {
return true;
}
try {
return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');
} catch {
return false;
}
}
export function getPackageJson() {
return require(path.resolve(process.cwd(), 'package.json'));
}
export function getPluginJson() {
return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`));
}
export function hasReadme() {
return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md'));
}
// Support bundling nested plugins by finding all plugin.json files in src directory
// then checking for a sibling module.[jt]sx? file.
export async function getEntries(): Promise<Record<string, string>> {
const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true });
const plugins = await Promise.all(
pluginsJson.map((pluginJson) => {
const folder = path.dirname(pluginJson);
return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true });
})
);
return plugins.reduce((result, modules) => {
return modules.reduce((result, module) => {
const pluginPath = path.dirname(module);
const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, '');
const entryName = pluginName === '' ? 'module' : `${pluginName}/module`;
result[entryName] = module;
return result;
}, result);
}, {});
}

View File

@ -0,0 +1,214 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-webpack-config
*/
import CopyWebpackPlugin from 'copy-webpack-plugin';
import ESLintPlugin from 'eslint-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import LiveReloadPlugin from 'webpack-livereload-plugin';
import path from 'path';
import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
import { Configuration } from 'webpack';
import { getPackageJson, getPluginJson, hasReadme, getEntries, isWSL } from './utils';
import { SOURCE_DIR, DIST_DIR } from './constants';
const pluginJson = getPluginJson();
const config = async (env): Promise<Configuration> => {
const baseConfig: Configuration = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
},
context: path.join(process.cwd(), SOURCE_DIR),
devtool: env.production ? 'source-map' : 'eval-source-map',
entry: await getEntries(),
externals: [
'lodash',
'jquery',
'moment',
'slate',
'emotion',
'@emotion/react',
'@emotion/css',
'prismjs',
'slate-plain-serializer',
'@grafana/slate-react',
'react',
'react-dom',
'react-redux',
'redux',
'rxjs',
'react-router',
'react-router-dom',
'd3',
'angular',
'@grafana/ui',
'@grafana/runtime',
'@grafana/data',
// Mark legacy SDK imports as external if their name starts with the "grafana/" prefix
({ request }, callback) => {
const prefix = 'grafana/';
const hasPrefix = (request) => request.indexOf(prefix) === 0;
const stripPrefix = (request) => request.substr(prefix.length);
if (hasPrefix(request)) {
return callback(undefined, stripPrefix(request));
}
callback();
},
],
mode: env.production ? 'production' : 'development',
module: {
rules: [
{
exclude: /(node_modules)/,
test: /\.[tj]sx?$/,
use: {
loader: 'swc-loader',
options: {
jsc: {
baseUrl: path.resolve(__dirname, 'src'),
target: 'es2015',
loose: false,
parser: {
syntax: 'typescript',
tsx: true,
decorators: false,
dynamicImport: true,
},
},
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.(png|jpe?g|gif|svg)$/,
type: 'asset/resource',
generator: {
// Keep publicPath relative for host.com/grafana/ deployments
publicPath: `public/plugins/${pluginJson.id}/img/`,
outputPath: 'img/',
filename: Boolean(env.production) ? '[hash][ext]' : '[file]',
},
},
{
test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/,
type: 'asset/resource',
generator: {
// Keep publicPath relative for host.com/grafana/ deployments
publicPath: `public/plugins/${pluginJson.id}/fonts/`,
outputPath: 'fonts/',
filename: Boolean(env.production) ? '[hash][ext]' : '[name][ext]',
},
},
],
},
output: {
clean: {
keep: new RegExp(`(.*?_(amd64|arm(64)?)(.exe)?|go_plugin_build_manifest)`),
},
filename: '[name].js',
library: {
type: 'amd',
},
path: path.resolve(process.cwd(), DIST_DIR),
publicPath: `public/plugins/${pluginJson.id}/`,
uniqueName: pluginJson.id,
},
plugins: [
new CopyWebpackPlugin({
patterns: [
// If src/README.md exists use it; otherwise the root README
// To `compiler.options.output`
{ from: hasReadme() ? 'README.md' : '../README.md', to: '.', force: true },
{ from: 'plugin.json', to: '.' },
{ from: '../LICENSE', to: '.' },
{ from: '../CHANGELOG.md', to: '.', force: true },
{ from: '**/*.json', to: '.' }, // TODO<Add an error for checking the basic structure of the repo>
{ from: '**/*.svg', to: '.', noErrorOnMissing: true }, // Optional
{ from: '**/*.png', to: '.', noErrorOnMissing: true }, // Optional
{ from: '**/*.html', to: '.', noErrorOnMissing: true }, // Optional
{ from: 'img/**/*', to: '.', noErrorOnMissing: true }, // Optional
{ from: 'libs/**/*', to: '.', noErrorOnMissing: true }, // Optional
{ from: 'static/**/*', to: '.', noErrorOnMissing: true }, // Optional
{ from: '**/query_help.md', to: '.', noErrorOnMissing: true }, // Optional
],
}),
// Replace certain template-variables in the README and plugin.json
new ReplaceInFileWebpackPlugin([
{
dir: DIST_DIR,
files: ['plugin.json', 'README.md'],
rules: [
{
search: /\%VERSION\%/g,
replace: getPackageJson().version,
},
{
search: /\%TODAY\%/g,
replace: new Date().toISOString().substring(0, 10),
},
{
search: /\%PLUGIN_ID\%/g,
replace: pluginJson.id,
},
],
},
]),
new ForkTsCheckerWebpackPlugin({
async: Boolean(env.development),
issue: {
include: [{ file: '**/*.{ts,tsx}' }],
},
typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') },
}),
new ESLintPlugin({
extensions: ['.ts', '.tsx'],
lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files
}),
...(env.development ? [new LiveReloadPlugin()] : []),
],
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
// handle resolving "rootDir" paths
modules: [path.resolve(process.cwd(), 'src'), 'node_modules'],
unsafeCache: true,
},
};
if (isWSL()) {
baseConfig.watchOptions = {
poll: 3000,
ignored: /node_modules/,
};
}
return baseConfig;
};
export default config;

3
.cprc.json Normal file
View File

@ -0,0 +1,3 @@
{
"features": {}
}

3
.eslintrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "./.config/.eslintrc"
}

View File

@ -4,72 +4,90 @@ on:
push:
branches:
- master
- main
pull_request:
branches:
- master
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
# pnpm action uses the packageManager field in package.json to
# understand which version to install.
- uses: pnpm/action-setup@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.2
uses: actions/setup-node@v3
with:
node-version: "14.x"
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache yarn cache
uses: actions/cache@v2
id: cache-yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.node-version }}-nodemodules-
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: yarn install --frozen-lockfile
run: pnpm install --frozen-lockfile --prefer-offline
- name: Build and test frontend
run: yarn build
- name: Check types
run: pnpm run typecheck
- name: Lint
run: pnpm run lint
- name: Unit tests
run: pnpm run test:ci
- name: Build frontend
run: pnpm run build
- name: Check for backend
id: check-for-backend
run: |
if [ -f "Magefile.go" ]
then
echo "::set-output name=has-backend::true"
echo "has-backend=true" >> $GITHUB_OUTPUT
fi
- name: Setup Go environment
if: steps.check-for-backend.outputs.has-backend == 'true'
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: "1.15"
go-version: '1.21'
- name: Test backend
if: steps.check-for-backend.outputs.has-backend == 'true'
uses: magefile/mage-action@v1
uses: magefile/mage-action@v2
with:
version: latest
args: coverage
- name: Build backend
if: steps.check-for-backend.outputs.has-backend == 'true'
uses: magefile/mage-action@v1
uses: magefile/mage-action@v2
with:
version: latest
args: buildAll
- name: Check for E2E
id: check-for-e2e
run: |
if [ -d "cypress" ]
then
echo "has-e2e=true" >> $GITHUB_OUTPUT
fi
- name: Start grafana docker
if: steps.check-for-e2e.outputs.has-e2e == 'true'
run: docker-compose up -d
- name: Run e2e tests
if: steps.check-for-e2e.outputs.has-e2e == 'true'
run: pnpm run e2e
- name: Stop grafana docker
if: steps.check-for-e2e.outputs.has-e2e == 'true'
run: docker-compose down
- name: Archive E2E output
uses: actions/upload-artifact@v3
if: steps.check-for-e2e.outputs.has-e2e == 'true' && steps.run-e2e-tests.outcome != 'success'
with:
name: cypress-videos
path: cypress/videos
retention-days: 5

22
.github/workflows/is-compatible.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Latest Grafana API compatibility check
on: [pull_request]
jobs:
compatibilitycheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# pnpm action uses the packageManager field in package.json to
# understand which version to install.
- uses: pnpm/action-setup@v2
- name: Setup Node.js environment
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline
- name: Build plugin
run: pnpm run build
- name: Compatibility check
run: npx @grafana/levitate@latest is-compatible --path src/module.ts --target @grafana/data,@grafana/ui,@grafana/runtime

View File

@ -3,162 +3,19 @@ name: Release
on:
push:
tags:
- "v*.*.*" # Run workflow on version tags, e.g. v1.0.0.
- 'v*' # Run workflow on version tags, e.g. v1.0.0.
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.2
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
node-version: "14.x"
- name: Setup Go environment
uses: actions/setup-go@v2
version: 8
- uses: grafana/plugin-actions/build-plugin@release
# uncomment to enable plugin sign in
with:
go-version: "1.15"
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache yarn cache
uses: actions/cache@v2
id: cache-yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.node-version }}-nodemodules-
- name: Install dependencies
run: yarn install --frozen-lockfile;
if: |
steps.cache-yarn-cache.outputs.cache-hit != 'true' ||
steps.cache-node-modules.outputs.cache-hit != 'true'
- name: Build and test frontend
run: yarn build
- name: Check for backend
id: check-for-backend
run: |
if [ -f "Magefile.go" ]
then
echo "::set-output name=has-backend::true"
fi
- name: Test backend
if: steps.check-for-backend.outputs.has-backend == 'true'
uses: magefile/mage-action@v1
with:
version: latest
args: coverage
- name: Build backend
if: steps.check-for-backend.outputs.has-backend == 'true'
uses: magefile/mage-action@v1
with:
version: latest
args: buildAll
- name: Sign plugin
run: yarn sign
env:
GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }} # Requires a Grafana API key from Grafana.com.
- name: Get plugin metadata
id: metadata
run: |
sudo apt-get install jq
export GRAFANA_PLUGIN_ID=$(cat dist/plugin.json | jq -r .id)
export GRAFANA_PLUGIN_VERSION=$(cat dist/plugin.json | jq -r .info.version)
export GRAFANA_PLUGIN_TYPE=$(cat dist/plugin.json | jq -r .type)
export GRAFANA_PLUGIN_ARTIFACT=${GRAFANA_PLUGIN_ID}-${GRAFANA_PLUGIN_VERSION}.zip
export GRAFANA_PLUGIN_ARTIFACT_CHECKSUM=${GRAFANA_PLUGIN_ARTIFACT}.md5
echo "::set-output name=plugin-id::${GRAFANA_PLUGIN_ID}"
echo "::set-output name=plugin-version::${GRAFANA_PLUGIN_VERSION}"
echo "::set-output name=plugin-type::${GRAFANA_PLUGIN_TYPE}"
echo "::set-output name=archive::${GRAFANA_PLUGIN_ARTIFACT}"
echo "::set-output name=archive-checksum::${GRAFANA_PLUGIN_ARTIFACT_CHECKSUM}"
echo ::set-output name=github-tag::${GITHUB_REF#refs/*/}
- name: Read changelog
id: changelog
run: |
awk '/^## / {s++} s == 1 {print}' CHANGELOG.md > release_notes.md
echo "::set-output name=path::release_notes.md"
- name: Check package version
run: if [ "v${{ steps.metadata.outputs.plugin-version }}" != "${{ steps.metadata.outputs.github-tag }}" ]; then printf "\033[0;31mPlugin version doesn't match tag name\033[0m\n"; exit 1; fi
- name: Package plugin
id: package-plugin
run: |
mv dist ${{ steps.metadata.outputs.plugin-id }}
zip ${{ steps.metadata.outputs.archive }} ${{ steps.metadata.outputs.plugin-id }} -r
md5sum ${{ steps.metadata.outputs.archive }} > ${{ steps.metadata.outputs.archive-checksum }}
echo "::set-output name=checksum::$(cat ./${{ steps.metadata.outputs.archive-checksum }} | cut -d' ' -f1)"
- name: Lint plugin
run: |
git clone https://github.com/grafana/plugin-validator
pushd ./plugin-validator/cmd/plugincheck
go install
popd
plugincheck ${{ steps.metadata.outputs.archive }}
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body_path: ${{ steps.changelog.outputs.path }}
draft: true
- name: Add plugin to release
id: upload-plugin-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ steps.metadata.outputs.archive }}
asset_name: ${{ steps.metadata.outputs.archive }}
asset_content_type: application/zip
- name: Add checksum to release
id: upload-checksum-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ steps.metadata.outputs.archive-checksum }}
asset_name: ${{ steps.metadata.outputs.archive-checksum }}
asset_content_type: text/plain
- name: Publish to Grafana.com
run: |
echo A draft release has been created for your plugin. Please review and publish it. Then submit your plugin to grafana.com/plugins by opening a PR to https://github.com/grafana/grafana-plugin-repository with the following entry:
echo
echo '{ "id": "${{ steps.metadata.outputs.plugin-id }}", "type": "${{ steps.metadata.outputs.plugin-type }}", "url": "https://github.com/${{ github.repository }}", "versions": [ { "version": "${{ steps.metadata.outputs.plugin-version }}", "commit": "${{ github.sha }}", "url": "https://github.com/${{ github.repository }}", "download": { "any": { "url": "https://github.com/${{ github.repository }}/releases/download/v${{ steps.metadata.outputs.plugin-version }}/${{ steps.metadata.outputs.archive }}", "md5": "${{ steps.package-plugin.outputs.checksum }}" } } } ] }' | jq .
# see https://grafana.com/developers/plugin-tools/publish-a-plugin/sign-a-plugin#generate-an-access-policy-token to generate it
# save the value in your repository secrets
policy_token: ${{ secrets.POLICY_TOKEN }}

5
.gitignore vendored
View File

@ -4,6 +4,7 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
node_modules/
@ -25,10 +26,14 @@ artifacts/
work/
ci/
e2e-results/
**/cypress/videos
**/cypress/report.json
# Editor
.idea
.eslintcache
# npm
package-lock.json

View File

@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

17
.npmrc Normal file
View File

@ -0,0 +1,17 @@
# This file is required for PNPM
# PNPM 8 changed the default resolution mode to "lowest-direct" which is not how we expect resolutions to work
resolution-mode="highest"
# Make sure the default patterns are still included (https://pnpm.io/npmrc#public-hoist-pattern)
public-hoist-pattern[]="*eslint*"
public-hoist-pattern[]="*prettier*"
# Hoist all types packages to the root for better TS support
public-hoist-pattern[]="@types/*"
# @grafana/e2e expects cypress to exist in the root of the node_modules directory
public-hoist-pattern[]="*cypress*"
public-hoist-pattern[]="lodash"
public-hoist-pattern[]="rxjs"

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
20

View File

@ -1,16 +1,10 @@
module.exports = {
...require('./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json'),
arrowParens: 'avoid',
importOrder: [
'<THIRD_PARTY_MODULES>',
'^@ui/(.*)$',
'store',
'^slices/(.*)$',
'^components/(.*)$',
'^lib/(.*)$',
'^images/(.*)$',
'^[./]',
],
// Prettier configuration provided by Grafana scaffolding
...require('./.config/.prettierrc.js'),
semi: false,
printWidth: 80,
importOrder: ['<THIRD_PARTY_MODULES>', '^[./]'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
plugins: ['@trivago/prettier-plugin-sort-imports'],
}

View File

@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.0.0] - 2023-12-18
### Added
- Support multi-value variables (for example, you can select multiple experiment names as a filter)
### Changed
Deprecate Angular support. Refer to [#55](https://github.com/chaos-mesh/datasource/issues/55) for more details.
Please also read the README for new information.
## [2.2.3] - 2022-08-07
### Fixed
- Update the outdated annotations screenshot.
## [2.2.2] - 2022-07-31
### Changed
Ready for submission to grafana official plugins repository. 🥰
## [2.2.1] - 2022-07-28
### Fixed
- Clone the annotation query before using it, which prevents mutating the original value if you use a variable in annotations.
- Reset the `kind` field to `input` in annotations, which allows you to use variables in the kind field, such as `$kind`.
## [2.2.0] - 2022-06-24
### Added
@ -24,7 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Compatible with Chaos Mesh 2.x. **(After 2.0.x, will start with 2.1.x)**
- Compatible with Chaos Mesh 2.x (**after 2.0.x, will start with 2.1.x**).
- Bump the minimal grafana version to 7.0.0
- Bump grafana/toolkit to 8.x
@ -38,6 +68,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Visualize Chaos Events on the table
- Show Chaos Events on the graph with [Annotations](https://grafana.com/docs/grafana/latest/dashboards/annotations/)
- Display different Chaos Events by [Variables](https://grafana.com/docs/grafana/latest/variables/)
- Displaying Chaos events in a table visualization
- Support [Variables](https://grafana.com/docs/grafana/latest/variables/) to filter Chaos events
- Support [Annotations](https://grafana.com/docs/grafana/latest/dashboards/annotations/) to annotate Chaos events on the panel

View File

@ -2,38 +2,41 @@
Grafana data source plugin for Chaos Mesh.
> Require: Chaos Mesh >= **2.1.0**, Grafana >= **7.0.0**
> This plugin requires Chaos Mesh **>=2.1**, Grafana >= **10.0**.
>
> Note: We only test the plugin on Grafana 10.0.3, it may support lower versions, but we are not sure.
> Upgrading to Grafana v10 is because of the [Angular support deprecation](https://github.com/chaos-mesh/datasource/issues/55). If you encounter any problems, please open an issue to let us know.
## Features
- Visualize Chaos Events on the table
- Show Chaos Events on the graph with [Annotations](https://grafana.com/docs/grafana/latest/dashboards/annotations/)
- Display different Chaos Events by [Variables](https://grafana.com/docs/grafana/latest/variables/)
- Displaying Chaos events in a table visualization
- Support [Variables](https://grafana.com/docs/grafana/latest/variables/) to filter Chaos events
- Support [Annotations](https://grafana.com/docs/grafana/latest/dashboards/annotations/) to annotate Chaos events on the panel
<!-- ## Install
## Installation
### With dashboard
[https://grafana.com/docs/grafana/latest/administration/plugin-management/#install-a-plugin](https://grafana.com/docs/grafana/latest/administration/plugin-management/#install-a-plugin)
### With cli
```sh
grafana-cli plugins install chaosmeshorg-datasource
``` -->
```
## Install
> **Note:**
>
> Because Grafana is not yet accepting the plugin submission for Chaos Mesh Data Source, it can't be installed using `grafana-cli` at this time.
>
> The following steps show how to install the Data Source plugin locally.
## Manual installation
Download the plugin zip package with the following command or go to <https://github.com/chaos-mesh/datasource/releases> to download:
```shell
curl -LO https://github.com/chaos-mesh/datasource/releases/download/v2.2.0/chaosmeshorg-datasource-2.2.0.zip
curl -LO https://github.com/chaos-mesh/datasource/releases/download/v3.0.0/chaosmeshorg-datasource-3.0.0.zip
```
After downloading, unzip:
```shell
unzip chaosmeshorg-datasource-2.2.0.zip -d YOUR_PLUGIN_DIR
unzip chaosmeshorg-datasource-3.0.0.zip -d YOUR_PLUGIN_DIR
```
Then update and save the `grafana.ini` file:
@ -47,23 +50,35 @@ Finally, restart Grafana to load the plugin.
## Setup
Once installed, go to **Configuration -> Data sources** and add Chaos Mesh, then go to the configuration page:
Once installed, go to **Administration -> Data sources** and add Chaos Mesh, then go to the configuration page:
![Settings](https://raw.githubusercontent.com/chaos-mesh/datasource/master/src/img/settings.png)
Assuming you have Chaos Mesh installed locally, Dashboard will export the API on port `2333` by default. So, if you haven't changed anything, you can just fill in `http://localhost:2333`.
Assuming you have Chaos Mesh installed locally, the Chaos Dashboard will export the API on port `2333` by default. So, if you haven't changed anything, you can fill in `http://localhost:2333`.
Then use the `port-forward` command to activate:
Then use the `port-forward` command to make the API externally accessible:
```shell
kubectl port-forward -n chaos-testing svc/chaos-dashboard 2333:2333
kubectl port-forward -n chaos-mesh svc/chaos-dashboard 2333:2333
```
Finally, click **Save & Test** to test the connection. If it shows a successful notification, the setup is complete.
Finally, click **Save & test** to test the connection. If it shows a successful notification, the setup is complete.
### Authentication
If you deploy Chaos Mesh with [permission authentication](https://chaos-mesh.org/docs/manage-user-permissions), you need to add the `Authorization` header to the configuration.
You can follow the steps below to add the header:
1. Click the **Add header** button.
2. Fill in the `Authorization` in the **Header** field.
3. Follow [this section](https://chaos-mesh.org/docs/manage-user-permissions/#get-the-token) to get the token.
4. Fill in the `Bearer YOUR_TOKEN` in the **Value** field.
Then don't forget to click **Save & test** to test the connection.
## Query
The Data Source plugin looks at the Chaos Mesh through the lens of events, and the following options are responsible for filtering the different events:
The data source plugin looks at the Chaos Mesh through the lens of events, and the following options are responsible for filtering the different events:
- **Object ID**
@ -79,48 +94,43 @@ The Data Source plugin looks at the Chaos Mesh through the lens of events, and t
- **Kind**
> Filter by kind (PodChaos, Schedule...).
> Filter by kind (PodChaos, NetworkChaos, Schedule...). You can also input an arbitrary kind
> if you implement a new kind in Chaos Mesh.
- **Limit**
> Limit the number of events.
They will be passed as parameters to the `/api/events` API.
## Annotations
You can integrate Chaos Mesh's events into the panel via Annotations, the following is a sample creation:
![Annotations](https://raw.githubusercontent.com/chaos-mesh/datasource/master/src/img/annotations.png)
Please refer to the contents of [Query](#query) to fill in the corresponding fields.
All of them will be passed as parameters to the `/api/events` API.
## Variables
If you choose the type to `Query` and select the data source to `Chaos Mesh`, you can retrieve
the variables by different metrics:
The data source plugin supports adding query variables by different metrics:
![Variables](https://raw.githubusercontent.com/chaos-mesh/datasource/master/src/img/variables.png)
- Namespace
- **Namespace**
> After selection, all available namespaces will show in **Preview of values** directly. Without other operations.
> After selection, all available namespaces will show in the **Preview of values** directly.
- Kind
- **Kind**
> Same as **Namespace**. Retrieve all kinds.
- Experiment
- **Experiment/Schedule/Workflow**
> Same as **Namespace**. Retrieve current all experiments.
> Same as **Namespace**. Retrieve current all experiments/schedules/workflows.
>
> You can also specify the `queries` to further filter the values,
> for example, `?namespace=default` will only retrieve the experiments/schedules/workflows in the `default` namespace.
- Schedule
## Annotations
> Same as **Namespace**. Retrieve current all schedules.
You can integrate events into panels via annotations, the following is a sample creation, it will retrieve all PodChaos events:
- Workflow
![Annotations](https://raw.githubusercontent.com/chaos-mesh/datasource/master/src/img/annotations.png)
> Same as **Namespace**. Retrieve current all workflows.
Please refer to [Query](#query) to fill in the corresponding fields.
## How to contribute
@ -128,4 +138,4 @@ Pull a request or open an issue to describe your changes or problems.
## License
Same as Chaos Mesh. Under Apache-2.0 License.
Same as Chaos Mesh. Under the Apache-2.0 License.

46
REVIEW.md Normal file
View File

@ -0,0 +1,46 @@
# Review Guide
This doc is mainly for reviewers.
For testing, a Chaos Mesh instance is required. In next section, we will show how to deploy a Chaos Mesh instance with Helm.
## Deploy Chaos Mesh
> Refer to <https://chaos-mesh.org/docs/production-installation-using-helm/> for more details.
Follow the steps below to deploy a minimal Chaos Mesh instance:
```bash
# Add the chart
helm repo add chaos-mesh https://charts.chaos-mesh.org
# Create a Chaos Mesh instance
# Refer to https://chaos-mesh.org/docs/production-installation-using-helm/#step-4-install-chaos-mesh-in-different-environments if you use containerd or other container runtimes.
helm install chaos-mesh chaos-mesh/chaos-mesh -n=chaos-mesh --create-namespace --set controllerManager.leaderElection.enabled=false,dashboard.securityMode=false
```
Verify the installation:
```bash
kubectl get pods -n chaos-mesh
```
Port-forward the dashboard:
```bash
kubectl port-forward svc/chaos-dashboard -n chaos-mesh 2333:2333
```
Then you can visit the dashboard at <http://localhost:2333>.
## Test data source
For data source testing, there must be some Chaos events are already created. You can finish this step by creating a Chaos experiment in the dashboard.
Navigate to `Experiments` at the left sidebar and click `New experiment` button. For example,
let's create a `Pod Failure` experiment. Select `Kubernetes` -> `Pod Fault` -> `Pod Failure`, then
choose `Namespace Selectors` in `Scope` to `default` (this means the experiment will be applied to
all pods in the `default` namespace). Finally, fill in a name like `pod-failure` and set `Duration` to `1m`,
and click `Submit` button to create the experiment.
Now you can test the data source. Start creating a query from here: <https://github.com/chaos-mesh/datasource?tab=readme-ov-file#query>.
At the same time, you can also view created Chaos events via <http://localhost:2333/#/events>.

View File

@ -1,20 +0,0 @@
VERSION=$1
ID=chaosmeshorg-datasource
rm -rf dist
echo "Bundled Version: $VERSION"
echo "Start to build..."
echo
yarn build
# yarn sign
echo "Bundling..."
cp -r dist $ID
zip -r $ID-$VERSION.zip dist -x "*.DS_Store*"
md5sum $ID-$VERSION.zip > $ID-$VERSION.zip.md5
rm -rf $ID
echo "Done."

3
cypress.json Normal file
View File

@ -0,0 +1,3 @@
{
"video": false
}

View File

@ -0,0 +1,10 @@
import { e2e } from '@grafana/e2e';
e2e.scenario({
describeName: 'Smoke test',
itName: 'Smoke test',
scenario: () => {
e2e.pages.Home.visit();
e2e().contains('Welcome to Grafana').should('be.visible');
},
});

16
docker-compose.yaml Normal file
View File

@ -0,0 +1,16 @@
version: '3.0'
services:
grafana:
container_name: 'chaosmeshorg-chaosmesh-datasource'
platform: 'linux/amd64'
build:
context: ./.config
args:
grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise}
grafana_version: ${GRAFANA_VERSION:-10.0.3}
ports:
- 3000:3000/tcp
volumes:
- ./dist:/var/lib/grafana/plugins/chaosmeshorg-chaosmesh-datasource
- ./provisioning:/etc/grafana/provisioning

2
jest-setup.js Normal file
View File

@ -0,0 +1,2 @@
// Jest setup provided by Grafana scaffolding
import './.config/jest-setup';

View File

@ -1,8 +1,8 @@
// This file is needed because it is used by vscode and other tools that
// call `jest` directly. However, unless you are doing anything special
// do not edit this file
// force timezone to UTC to allow tests to work regardless of local timezone
// generally used by snapshots, but can affect specific tests
process.env.TZ = 'UTC';
const standard = require('@grafana/toolkit/src/config/jest.plugin.config');
// This process will use the same config that `yarn test` is using
module.exports = standard.jestConfig();
module.exports = {
// Jest configuration provided by Grafana scaffolding
...require('./.config/jest.config'),
};

View File

@ -1,36 +1,82 @@
{
"name": "chaosmeshorg-datasource",
"version": "2.2.0",
"description": "Chaos Mesh (A Chaos Engineering Platform for Kubernetes) Data source",
"version": "3.0.0",
"description": "Grafana data source plugin for Chaos Mesh (A Chaos Engineering Platform for Kubernetes)",
"scripts": {
"build": "grafana-toolkit plugin:build",
"test": "grafana-toolkit plugin:test",
"dev": "grafana-toolkit plugin:dev",
"watch": "grafana-toolkit plugin:dev --watch",
"sign": "grafana-toolkit plugin:sign",
"start": "yarn watch",
"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
"dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development",
"test": "jest --watch --onlyChanged",
"test:ci": "jest --passWithNoTests --maxWorkers 4",
"typecheck": "tsc --noEmit",
"lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .",
"lint:fix": "pnpm run lint --fix",
"e2e": "pnpm exec cypress install && pnpm exec grafana-e2e run",
"e2e:update": "pnpm exec cypress install && pnpm exec grafana-e2e run --update-screenshots",
"server": "docker-compose up --build",
"sign": "npx --yes @grafana/sign-plugin@latest",
"prepare": "husky install"
},
"author": "Yue Yang <g1enyy0ung@gmail.com>",
"license": "Apache-2.0",
"devDependencies": {
"@grafana/data": "7.0.x",
"@grafana/runtime": "^8.2.2",
"@grafana/toolkit": "^8.2.2",
"@grafana/ui": "7.0.x",
"@testing-library/jest-dom": "5.4.0",
"@testing-library/react": "^10.0.2",
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
"@types/lodash": "^4.14.176",
"husky": "^7.0.4",
"lint-staged": "^11.2.4",
"prettier": "^2.4.1",
"react": "16.12.0",
"react-dom": "16.12.0"
"@babel/core": "^7.21.4",
"@grafana/e2e": "10.0.3",
"@grafana/e2e-selectors": "10.0.3",
"@grafana/eslint-config": "^6.0.0",
"@grafana/tsconfig": "^1.2.0-rc1",
"@swc/core": "^1.3.90",
"@swc/helpers": "^0.5.0",
"@swc/jest": "^0.2.26",
"@testing-library/jest-dom": "6.1.4",
"@testing-library/react": "14.0.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/jest": "^29.5.0",
"@types/lodash": "^4.14.194",
"@types/node": "^20.8.7",
"@types/testing-library__jest-dom": "5.14.8",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-webpack-plugin": "^4.0.1",
"fork-ts-checker-webpack-plugin": "^8.0.0",
"glob": "^10.2.7",
"husky": "^8.0.3",
"identity-obj-proxy": "3.0.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"lint-staged": "^15.2.0",
"prettier": "^2.8.7",
"replace-in-file-webpack-plugin": "^1.0.6",
"sass": "1.63.2",
"sass-loader": "13.3.1",
"style-loader": "3.3.3",
"swc-loader": "^0.2.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "4.8.4",
"webpack": "^5.86.0",
"webpack-cli": "^5.1.4",
"webpack-livereload-plugin": "^3.0.2"
},
"engines": {
"node": ">=14"
"node": ">=20"
},
"dependencies": {
"@emotion/css": "11.10.6",
"@grafana/data": "10.0.3",
"@grafana/runtime": "10.0.3",
"@grafana/schema": "10.0.3",
"@grafana/ui": "10.0.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"tslib": "2.5.3"
},
"pnpm": {
"overrides": {
"underscore": "^1.13.6"
}
},
"packageManager": "pnpm@8.12.1",
"lint-staged": {
"*.ts?(x)": "prettier --write"
}

11145
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

1
provisioning/README.md Normal file
View File

@ -0,0 +1 @@
For more information see [Provision dashboards and data sources](https://grafana.com/tutorials/provision-dashboards-and-data-sources/)

View File

View File

@ -0,0 +1,8 @@
apiVersion: 1
datasources:
- name: 'Chaos Mesh'
type: 'chaosmeshorg-datasource'
access: proxy
isDefault: true
editable: true

View File

@ -1 +1 @@
type uuid = string;
type uuid = string

View File

@ -1,32 +0,0 @@
/*
* Copyright 2022 Chaos Mesh Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { defaultQuery, kinds } from './types';
export class AnnotationQueryEditor {
static templateUrl = 'partials/annotations.editor.html';
annotation: any;
kinds = kinds;
constructor() {
this.annotation.object_id = this.annotation.object_id || '';
this.annotation.namespace = this.annotation.namespace || '';
this.annotation.eventName = this.annotation.eventName || ''; // There is a conflict with annotation name, so rename it to eventName.
this.annotation.kind = this.annotation.kind || kinds[0];
this.annotation.limit = this.annotation.limit || defaultQuery.limit;
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2022 Chaos Mesh Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { DataSourcePluginOptionsEditorProps, SelectableValue } from '@grafana/data';
import { LegacyForms } from '@grafana/ui';
import React, { SyntheticEvent } from 'react';
import { ChaosMeshOptions } from './types';
const { Input, FormField } = LegacyForms;
type Props = Pick<DataSourcePluginOptionsEditorProps<ChaosMeshOptions>, 'options' | 'onOptionsChange'>;
export const ChaosMeshSettings = (props: Props) => {
const { options, onOptionsChange } = props;
return (
<>
<h3 className="page-heading">Events</h3>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
<FormField
label="Limit"
labelWidth={12}
inputEl={
<Input
className="width-6"
value={options.jsonData.limit}
spellCheck={false}
placeholder="300"
onChange={onChangeHandler('limit', options, onOptionsChange)}
/>
}
tooltip="Limit the number of events to be fetched from the Chaos Mesh server. If not set, the default value is 300."
/>
</div>
</div>
</div>
</>
);
};
// https://github.com/grafana/grafana/blob/v9.0.1/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx#L152
export const getValueFromEvent = (e: SyntheticEvent<HTMLInputElement> | SelectableValue<string>) => {
if (!e) {
return '';
}
if (e.hasOwnProperty('currentTarget')) {
return e.currentTarget.value;
}
return (e as SelectableValue<string>).value;
};
const onChangeHandler =
(key: keyof ChaosMeshOptions, options: Props['options'], onOptionsChange: Props['onOptionsChange']) =>
(e: SyntheticEvent<HTMLInputElement> | SelectableValue<string>) => {
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
[key]: getValueFromEvent(e),
},
});
};

View File

@ -1,41 +0,0 @@
/*
* Copyright 2022 Chaos Mesh Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { DataSourceHttpSettings } from '@grafana/ui';
import { ChaosMeshSettings } from 'ChaosMeshSettings';
import React, { PureComponent } from 'react';
import { ChaosMeshOptions } from './types';
type Props = DataSourcePluginOptionsEditorProps<ChaosMeshOptions>;
export class ConfigEditor extends PureComponent<Props> {
render() {
const { options, onOptionsChange } = this.props;
return (
<>
<DataSourceHttpSettings
defaultUrl="http://localhost:2333"
dataSourceConfig={options}
onChange={onOptionsChange}
/>
<ChaosMeshSettings options={options} onOptionsChange={onOptionsChange} />
</>
);
}
}

View File

@ -1,81 +0,0 @@
/*
* Copyright 2022 Chaos Mesh Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { QueryEditorProps } from '@grafana/data';
import { LegacyForms } from '@grafana/ui';
import _, { DebouncedFunc } from 'lodash';
import React, { ChangeEvent, PureComponent } from 'react';
import { DataSource } from './datasource';
import { ChaosMeshOptions, EventsQuery, defaultQuery } from './types';
const { FormField } = LegacyForms;
type Props = QueryEditorProps<DataSource, EventsQuery, ChaosMeshOptions>;
export class QueryEditor extends PureComponent<Props> {
onRunQueryDebounced: DebouncedFunc<any>;
constructor(props: Props) {
super(props);
this.onRunQueryDebounced = _.debounce(this.props.onRunQuery, 500);
}
onChange = (key: keyof EventsQuery) => (event: ChangeEvent<HTMLInputElement>) => {
const { onChange, query } = this.props;
let value: string | number = event.target.value;
if (key === 'limit') {
value = parseInt(value, 10);
}
onChange({ ...query, [key]: value });
// executes the query
this.onRunQueryDebounced();
};
render() {
const query = _.defaults(this.props.query, defaultQuery);
const { object_id, namespace, name, kind, limit } = query;
return (
<div className="gf-form">
<FormField
value={object_id || ''}
onChange={this.onChange('object_id')}
label="Object ID"
tooltip="Filter events by Object ID."
/>
<FormField
value={namespace || ''}
onChange={this.onChange('namespace')}
label="Namespace"
tooltip="Filter events by Namespace."
/>
<FormField value={name || ''} onChange={this.onChange('name')} label="Name" tooltip="Filter events by Name." />
<FormField value={kind || ''} onChange={this.onChange('kind')} label="Kind" tooltip="Filter events by Kind." />
<FormField
type="number"
value={limit}
onChange={this.onChange('limit')}
label="Limit"
tooltip="Limit the number of events to be fetched from the Chaos Mesh server."
/>
</div>
);
}
}

View File

@ -1,93 +0,0 @@
/*
* Copyright 2022 Chaos Mesh Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { SelectableValue } from '@grafana/data';
import { InlineFormLabel, LegacyForms, Select } from '@grafana/ui';
import _ from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { VariableQuery } from './types';
const { FormField, Input } = LegacyForms;
type Option = SelectableValue<VariableQuery['metric']>;
const metricOptions: Option[] = [
{ label: 'Namespace', value: 'namespace', description: 'Retrieve current all namespaces.' },
{ label: 'Kind', value: 'kind', description: 'Retrieve all chaos kinds.' },
{ label: 'Experiment', value: 'experiment', description: 'Retrieve current all experiments.' },
{ label: 'Schedule', value: 'schedule', description: 'Retrieve current all schedules.' },
{ label: 'Workflow', value: 'workflow', description: 'Retrieve current all workflows.' },
];
interface VariableQueryProps {
query: VariableQuery;
onChange: (query: VariableQuery, definition: string) => void;
}
export const VariableQueryEditor: React.FC<VariableQueryProps> = ({ query, onChange }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedOnChange = useCallback(_.debounce(onChange, 500), []);
const [state, setState] = useState(query);
useEffect(() => {
debouncedOnChange(state, `metric: ${state.metric}`);
}, [debouncedOnChange, state]);
const onMetricChange = (option: Option) => {
setState({ ...state, metric: option.value! });
};
const onQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setState({ ...state, queryString: e.target.value });
};
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
width={10}
tooltip="Select different metric to generate different sets of variables. Each of these metrics is exactly what its name says."
>
Metric
</InlineFormLabel>
<Select width={25} options={metricOptions} value={state.metric} onChange={onMetricChange} />
</div>
</div>
{['experiment', 'schedule', 'workflow'].includes(state.metric) && (
<div className="gf-form-inline">
<div className="gf-form">
<FormField
label="Queries"
labelWidth={10}
inputEl={
<Input
className="width-13"
value={state.queryString}
spellCheck={false}
placeholder="?namespace=default"
onChange={onQueryChange}
/>
}
tooltip="Add a query string to the Metric API."
/>
</div>
</div>
)}
</>
);
};

View File

@ -14,21 +14,21 @@
* limitations under the License.
*
*/
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { ChaosMeshSettings } from 'ChaosMeshSettings';
import React from 'react';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data'
import { DataSourceHttpSettings } from '@grafana/ui'
import React from 'react'
const defaultOptions = {
jsonData: {
limit: 300,
},
};
import { ChaosMeshOptions } from '../types'
describe('<ChaosMeshSettings />', () => {
it('loaded successfully', () => {
render(<ChaosMeshSettings options={defaultOptions as any} onOptionsChange={() => {}} />);
expect(screen.getByText('Limit')).toBeInTheDocument();
});
});
export function ConfigEditor({
options,
onOptionsChange,
}: DataSourcePluginOptionsEditorProps<ChaosMeshOptions>) {
return (
<DataSourceHttpSettings
defaultUrl="http://localhost:2333"
dataSourceConfig={options}
onChange={onOptionsChange}
/>
)
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2023 Chaos Mesh Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { QueryEditorProps, SelectableValue } from '@grafana/data'
import { InlineField, Input, Select } from '@grafana/ui'
import React, { ChangeEvent } from 'react'
import { DataSource } from '../datasource'
import { ChaosMeshOptions, EventQuery, kindOptions } from '../types'
type Props = QueryEditorProps<DataSource, EventQuery, ChaosMeshOptions>
export function QueryEditor({ query, onChange, onRunQuery }: Props) {
const onInputChange =
(key: keyof EventQuery) => (event: ChangeEvent<HTMLInputElement>) => {
let value: any = event.target.value
if (key === 'limit') {
value = parseInt(value, 10)
}
onChange({ ...query, [key]: value })
// executes the query
onRunQuery()
}
const onSelectChange =
(key: keyof EventQuery) => (option: SelectableValue<string> | null) => {
onChange({ ...query, [key]: option ? option.value : undefined })
// executes the query
onRunQuery()
}
const { object_id, namespace, name, kind, limit } = query
return (
<div className="gf-form">
<InlineField label="Object ID" tooltip="Filter events by object ID">
<Input value={object_id} onChange={onInputChange('object_id')} />
</InlineField>
<InlineField label="Namespace" tooltip="Filter events by namespace">
<Input value={namespace} onChange={onInputChange('namespace')} />
</InlineField>
<InlineField label="Name" tooltip="Filter events by name">
<Input value={name} onChange={onInputChange('name')} />
</InlineField>
<InlineField label="Kind" tooltip="Filter events by kind">
<Select
value={kind}
options={kindOptions}
onChange={onSelectChange('kind')}
isClearable
allowCustomValue
/>
</InlineField>
<InlineField
label="Limit"
tooltip="Limit the number of events to be fetched from the server"
>
<Input type="number" value={limit} onChange={onInputChange('limit')} />
</InlineField>
</div>
)
}

View File

@ -0,0 +1,105 @@
/*
* Copyright 2022 Chaos Mesh Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { SelectableValue } from '@grafana/data'
import { InlineField, Input, Select } from '@grafana/ui'
import _ from 'lodash'
import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'
import { ChaosMeshVariableQuery } from '../types'
type Option = SelectableValue<ChaosMeshVariableQuery['metric']>
const metricOptions: Option[] = [
{
label: 'Namespace',
value: 'namespace',
description: 'Retrieve all namespaces',
},
{ label: 'Kind', value: 'kind', description: 'Retrieve all chaos kinds' },
{
label: 'Experiment',
value: 'experiment',
description: 'Retrieve experiments',
},
{
label: 'Schedule',
value: 'schedule',
description: 'Retrieve schedules',
},
{
label: 'Workflow',
value: 'workflow',
description: 'Retrieve workflows',
},
]
interface VariableQueryProps {
query: ChaosMeshVariableQuery
onChange: (query: ChaosMeshVariableQuery, definition: string) => void
}
export const VariableQueryEditor = ({
onChange,
query,
}: VariableQueryProps) => {
const debouncedOnChange = useMemo(() => _.debounce(onChange, 300), [onChange])
const [state, setState] = useState(query)
useEffect(() => {
debouncedOnChange(state, `metric: ${state.metric}`)
}, [debouncedOnChange, state])
const onMetricChange = (
option: SelectableValue<ChaosMeshVariableQuery['metric']>
) => {
setState({ ...state, metric: option.value! })
}
const onQueryChange = (e: ChangeEvent<HTMLInputElement>) => {
setState({ ...state, queryString: e.target.value })
}
return (
<div className="gf-form">
<InlineField
label="Metric"
tooltip="Select a metric to generate different sets of variable"
>
<Select
width={30}
options={metricOptions}
value={state.metric}
onChange={onMetricChange}
/>
</InlineField>
{['experiment', 'schedule', 'workflow'].includes(state.metric) && (
<InlineField
label="Queries"
tooltip="Add a query string to the metric API"
>
<Input
width={30}
value={state.queryString}
placeholder="?namespace=default"
onChange={onQueryChange}
/>
</InlineField>
)}
</div>
)
}

View File

@ -16,188 +16,240 @@
*/
import {
AnnotationEvent,
AnnotationQueryRequest,
AnnotationQuery,
AnnotationSupport,
DataFrame,
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
FieldType,
MutableDataFrame,
ScopedVars,
} from '@grafana/data';
import { getBackendSrv, getTemplateSrv } from '@grafana/runtime';
import _ from 'lodash';
import { lastValueFrom } from 'rxjs';
} from '@grafana/data'
import {
BackendSrvRequest,
getBackendSrv,
isFetchError,
} from '@grafana/runtime'
import _ from 'lodash'
import { lastValueFrom, of } from 'rxjs'
import { processMultipleVariables, processVariables } from 'utils'
import { ChaosMeshOptions, Event, EventsQuery, VariableQuery, defaultQuery, kinds } from './types';
import {
ChaosMeshOptions,
ChaosMeshVariableQuery,
Event,
EventQuery,
kinds,
} from './types'
const timeformat = 'YYYY-MM-DDTHH:mm:ssZ';
export class DataSource extends DataSourceApi<EventsQuery, ChaosMeshOptions> {
readonly url?: string;
readonly defaultQuery = defaultQuery;
readonly legacyFetch: boolean = false;
export class DataSource extends DataSourceApi<EventQuery, ChaosMeshOptions> {
readonly baseUrl: string
readonly fields = [
{
name: 'object_id',
type: FieldType.string,
config: { displayName: 'Object ID' },
},
{
name: 'namespace',
type: FieldType.string,
config: { displayName: 'Namespace' },
},
{
name: 'name',
type: FieldType.string,
config: { displayName: 'Name' },
},
{
name: 'kind',
type: FieldType.string,
config: { displayName: 'Kind' },
},
{
name: 'created_at',
type: FieldType.time,
config: { displayName: 'Time' },
},
{
name: 'type',
type: FieldType.string,
config: { displayName: 'Type' },
},
{
name: 'reason',
type: FieldType.string,
config: { displayName: 'Reason' },
},
{
name: 'message',
type: FieldType.string,
config: { displayName: 'Message' },
},
]
constructor(instanceSettings: DataSourceInstanceSettings<ChaosMeshOptions>) {
super(instanceSettings);
super(instanceSettings)
this.url = instanceSettings.url + '/api';
if (instanceSettings.jsonData.limit) {
this.defaultQuery.limit = instanceSettings.jsonData.limit;
this.baseUrl = instanceSettings.url + '/api'
}
if (typeof getBackendSrv().fetch !== 'function') {
this.legacyFetch = true;
}
}
private fetch<T>(url: string, query?: Partial<EventsQuery>) {
const options = {
private fetch<T>(options: BackendSrvRequest) {
const _options = _.defaults(
{
url: this.baseUrl + options.url,
},
options,
{
method: 'GET',
url: this.url + url,
params: _.defaults(query, this.defaultQuery),
};
}
)
return (
!this.legacyFetch
? lastValueFrom(getBackendSrv().fetch<T>(options))
: getBackendSrv().datasourceRequest<T>(options)
).then(({ data }) => data);
const resp = getBackendSrv().fetch<T>(_options)
return lastValueFrom(resp)
}
private fetchEvents(query: Partial<EventsQuery>) {
return this.fetch<Event[]>('/events', query);
private fetchEvents(query: Partial<EventQuery>) {
return this.fetch<Event[]>({
url: '/events',
params: query,
})
}
/**
* This function use a tricky way to apply all the variables to the query.
*
* It firstly joins all values to a string and then replace by scoped vars, then zip the result to a object.
*
* @param query
* @param scopedVars
* @returns
*/
private applyVariables(query: EventsQuery, scopedVars: ScopedVars) {
const keys = _.keys(query);
const values = getTemplateSrv().replace(_.values(query).join('|'), scopedVars).split('|');
return _.zipObject(keys, values) as unknown as EventsQuery;
}
async query(options: DataQueryRequest<EventsQuery>): Promise<DataQueryResponse> {
const { range, scopedVars } = options;
const from = range.from.utc().format(timeformat);
const to = range.to.utc().format(timeformat);
async query(
options: DataQueryRequest<EventQuery>
): Promise<DataQueryResponse> {
const { range, scopedVars } = options
const from = range.from.toISOString()
const to = range.to.toISOString()
return Promise.all(
options.targets.map(async query => {
query.start = from;
query.end = to;
options.targets.map(async (query) => {
processVariables(query, scopedVars)
query.start = from
query.end = to
const queries = processMultipleVariables(query)
const frame = new MutableDataFrame<Event>({
refId: query.refId,
fields: [
{ name: 'object_id', type: FieldType.string },
{ name: 'namespace', type: FieldType.string },
{ name: 'name', type: FieldType.string },
{ name: 'kind', type: FieldType.string },
{ name: 'created_at', type: FieldType.time },
{ name: 'type', type: FieldType.string },
{ name: 'reason', type: FieldType.string },
{ name: 'message', type: FieldType.string },
],
});
(await this.fetchEvents(this.applyVariables(query, scopedVars))).forEach(d => frame.add(d));
return frame;
fields: this.fields,
})
).then(data => ({ data }));
for (const q of queries) {
;(await this.fetchEvents(q)).data.forEach((d) => frame.add(d))
}
return frame
})
).then((data) => ({ data }))
}
async testDatasource() {
return getBackendSrv()
.get(this.url + '/common/config')
.then(() => {
const defaultErrorMessage = 'Cannot connect to API'
try {
const resp = await this.fetch({
url: '/common/config',
})
if (resp.status === 200) {
return {
status: 'success',
message: 'Chaos Mesh API status is normal.',
};
})
.catch(error => {
const { data } = error;
message: 'Chaos Mesh API is available',
}
} else {
return {
status: 'error',
message: data ? data.message : error.statusText ? error.statusText : 'Chaos Mesh API is not available.',
};
});
message:
`Status code: ${resp.status}.` +
' Chaos Mesh API is not available.',
}
async annotationQuery(options: AnnotationQueryRequest<EventsQuery>): Promise<AnnotationEvent[]> {
const { range, annotation: query } = options;
const from = range.from.utc().format(timeformat);
const to = range.to.utc().format(timeformat);
const vars = _.mapValues(
_.keyBy(getTemplateSrv().getVariables(), d => '$' + d.name),
'current.value'
);
for (const q in query) {
if (!query.hasOwnProperty(q)) {
continue;
}
const val = (query as any)[q];
if (typeof val === 'string' && val.startsWith('$') && vars[val]) {
(query as any)[q] = vars[val];
} catch (err) {
let message = ''
if (_.isString(err)) {
message = err
} else if (isFetchError(err)) {
message =
'Fetch error: ' +
(err.statusText ? err.statusText : defaultErrorMessage)
if (err.data && err.data.error && err.data.error.code) {
message += ': ' + err.data.error.code + '. ' + err.data.error.message
}
}
return {
status: 'error',
message,
}
}
}
query.start = from;
query.end = to;
annotations: AnnotationSupport<EventQuery> = {
processEvents: (anno, data) => {
return of(this.eventFrameToAnnotation(anno, data))
},
}
return this.fetchEvents({ ...query, name: (query as any).eventName }).then(data => {
const grouped = _.groupBy(data, 'name');
return _.entries(grouped).map(([k, v]) => {
const first = v[v.length - 1];
const last = v[0];
private eventFrameToAnnotation(
anno: AnnotationQuery<EventQuery>,
data: DataFrame[]
): AnnotationEvent[] {
return data.map((frame) => {
const times = frame.fields
.find((f) => f.name === 'created_at')!
.values.reverse() // Default descending order. So reverse it.
const startTime = Date.parse(times[0])
const endTime = Date.parse(times[times.length - 1])
const names = frame.fields
.find((f) => f.name === 'name')!
.values.reverse()
const messages = frame.fields
.find((f) => f.name === 'message')!
.values.reverse()
return {
title: `<h6>${k}</h6>`,
text: v
.map(d => `<div>${new Date(d.created_at).toLocaleString()}: ${d.message}</div>`)
.reverse()
.join('\n'),
tags: [`namespace:${first.namespace}`, `kind:${first.kind}`],
time: Date.parse(first.created_at),
timeEnd: Date.parse(last.created_at),
};
});
});
title: `<div><b>${anno.name}</b></div>`,
time: startTime,
timeEnd: endTime,
text: `<ul style="max-height: 500px; margin-bottom: 1rem; overflow: auto;">
${messages
.map(
(d, i) =>
`<li style="margin-bottom: .5rem;">
<div>${i + 1}: ${d}</div>
<div>
<span class="label">${names[i]}</span>
<span class="label">${new Date(
times[i]
).toLocaleTimeString()}</span>
</div>
</li>`
)
.join('')}
</ul>`,
tags: ['Chaos Mesh'],
}
})
}
async metricFindQuery(query: VariableQuery) {
async metricFindQuery(query: ChaosMeshVariableQuery) {
switch (query.metric) {
case 'namespace':
return this.fetch<string[]>('/common/namespaces').then(data => data.map(d => ({ text: d })));
return this.fetch<string[]>({
url: '/common/chaos-available-namespaces',
}).then(({ data }) => data.map((d) => ({ text: d })))
case 'kind':
return kinds.map(d => ({ text: d }));
return kinds.map((d) => ({ text: d }))
case 'experiment':
case 'schedule':
case 'workflow':
return this.fetch<Array<{ name: string }>>(`/${query.metric}s${query.queryString || ''}`).then(data =>
data.map(d => ({ text: d.name }))
);
return this.fetch<Array<{ name: string }>>({
url: `/${query.metric}s${query.queryString || ''}`,
}).then(({ data }) => data.map((d) => ({ text: d.name })))
default:
return [];
return []
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -14,17 +14,19 @@
* limitations under the License.
*
*/
import { DataSourcePlugin } from '@grafana/data';
import { AnnotationQueryEditor } from 'AnnotationQueryEditor';
import { DataSourcePlugin } from '@grafana/data'
import { ConfigEditor } from './ConfigEditor';
import { QueryEditor } from './QueryEditor';
import { VariableQueryEditor } from './VariableQueryEditor';
import { DataSource } from './datasource';
import { ChaosMeshOptions, EventsQuery } from './types';
import { ConfigEditor } from './components/ConfigEditor'
import { QueryEditor } from './components/QueryEditor'
import { VariableQueryEditor } from './components/VariableQueryEditor'
import { DataSource } from './datasource'
import { ChaosMeshOptions, EventQuery } from './types'
export const plugin = new DataSourcePlugin<DataSource, EventsQuery, ChaosMeshOptions>(DataSource)
export const plugin = new DataSourcePlugin<
DataSource,
EventQuery,
ChaosMeshOptions
>(DataSource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(QueryEditor)
.setVariableQueryEditor(VariableQueryEditor)
.setAnnotationQueryCtrl(AnnotationQueryEditor);

View File

@ -1,30 +0,0 @@
<div class="gf-form-group">
<div class="gf-form">
<label class="gf-form-label width-6">Object ID</label>
<input class="gf-form-input width-12" ng-model="ctrl.annotation.object_id" />
</div>
<div class="gf-form">
<label class="gf-form-label width-6">Namespace</label>
<input class="gf-form-input width-12" ng-model="ctrl.annotation.namespace" />
</div>
<div class="gf-form">
<label class="gf-form-label width-6">Name</label>
<input class="gf-form-input width-12" ng-model="ctrl.annotation.eventName" />
</div>
<div class="gf-form">
<label class="gf-form-label width-6">Kind</label>
<select
class="gf-form-input width-12"
ng-model="ctrl.annotation.kind"
ng-options="kind for kind in ctrl.kinds"
></select>
</div>
<div class="gf-form">
<label class="gf-form-label width-6">Limit</label>
<input class="gf-form-input width-12" ng-model="ctrl.annotation.limit" />
</div>
</div>

View File

@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json",
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
"type": "datasource",
"name": "Chaos Mesh",
"id": "chaosmeshorg-datasource",
@ -7,12 +7,12 @@
"annotations": true,
"metrics": true,
"info": {
"description": "Chaos Mesh (A Chaos Engineering Platform for Kubernetes) Datasource",
"description": "Grafana data source plugin for Chaos Mesh (A Chaos Engineering Platform for Kubernetes)",
"author": {
"name": "Chaos Mesh Project Team",
"url": "https://github.com/chaos-mesh"
},
"keywords": ["Cloud", "Chaos Engineering", "Chaos Mesh"],
"keywords": ["datasource", "Cloud", "Chaos Engineering", "Chaos Mesh"],
"logos": {
"small": "img/logo.svg",
"large": "img/logo.svg"
@ -36,20 +36,20 @@
"name": "Settings",
"path": "img/settings.png"
},
{
"name": "Annotations",
"path": "img/annotations.png"
},
{
"name": "Variables",
"path": "img/variables.png"
},
{
"name": "Annotations",
"path": "img/annotations.png"
}
],
"version": "2.2.0",
"version": "%VERSION%",
"updated": "%TODAY%"
},
"dependencies": {
"grafanaDependency": ">=7.0.0",
"grafanaDependency": ">=10.0.3",
"plugins": []
}
}

View File

@ -14,32 +14,30 @@
* limitations under the License.
*
*/
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { DataSourceJsonData } from '@grafana/data'
import { DataQuery } from '@grafana/schema'
interface EventBase {
object_id: uuid;
namespace: string;
name: string;
kind: string;
object_id: uuid
namespace: string
name: string
kind: string
}
export interface Event extends EventBase {
created_at: string;
type: 'Normal' | 'Warning';
reason: string;
message: string;
created_at: string
type: string
reason: string
message: string
}
export interface EventsQuery extends DataQuery, EventBase {
start: string;
end: string;
limit?: number;
export interface EventQuery extends DataQuery, EventBase {
start: string
end: string
limit?: number
[key: string]: any
}
export const defaultQuery: Partial<EventsQuery> = {
limit: 300,
};
export const kinds = [
'AWSChaos',
'AzureChaos',
@ -55,16 +53,16 @@ export const kinds = [
'StressChaos',
'TimeChaos',
'PhysicalMachineChaos',
];
]
export interface VariableQuery {
metric: 'namespace' | 'kind' | 'experiment' | 'schedule' | 'workflow';
queryString?: string;
export const kindOptions = kinds.map((kind) => ({ label: kind, value: kind }))
export interface ChaosMeshVariableQuery {
metric: 'namespace' | 'kind' | 'experiment' | 'schedule' | 'workflow'
queryString?: string
}
/**
* These are options configured for each DataSource instance
*/
export interface ChaosMeshOptions extends DataSourceJsonData {
limit?: number;
}
export interface ChaosMeshOptions extends DataSourceJsonData {}

85
src/utils.ts Normal file
View File

@ -0,0 +1,85 @@
/*
* Copyright 2024 Chaos Mesh Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import type { ScopedVars } from '@grafana/data'
import { getTemplateSrv } from '@grafana/runtime'
import { EventQuery } from 'types'
/**
* Replace variables in the query with their real values.
*/
export function processVariables(query: EventQuery, scopedVars: ScopedVars) {
for (const [k, v] of Object.entries(query)) {
if (typeof v === 'string' && v.startsWith('$')) {
const val = getTemplateSrv().replace(query[k], scopedVars, 'json')
if (val.startsWith('[') && val.endsWith(']')) {
query[k] = JSON.parse(val)
} else {
query[k] = val
}
}
}
}
/**
* Process multiple variables in the query.
*
* This function must call after `processVariables`. Multiple variables will be
* transformed into a values array.
*/
export function processMultipleVariables(query: EventQuery) {
// Helper function to generate all combinations of arrays
function generateCombinations(
properties: string[],
currentIndex: number,
currentCombination: EventQuery,
result: EventQuery[]
) {
if (currentIndex === properties.length) {
// Include non-array properties in the final result
for (const key in query) {
if (!properties.includes(key)) {
currentCombination[key] = query[key]
}
}
result.push(currentCombination)
return
}
const currentProperty = properties[currentIndex]
const arrayValues = query[currentProperty]
for (const item of arrayValues) {
const newCombination = { ...currentCombination, [currentProperty]: item }
generateCombinations(properties, currentIndex + 1, newCombination, result)
}
}
// Get an array of property names with array values
const arrayProperties = Object.keys(query).filter((key) =>
Array.isArray(query[key])
)
// Generate all combinations
const combinations: EventQuery[] = []
generateCombinations(arrayProperties, 0, {} as EventQuery, combinations)
return combinations
}

View File

@ -1,9 +1,3 @@
{
"extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json",
"include": ["src", "types"],
"compilerOptions": {
"rootDir": "./src",
"baseUrl": "./src",
"typeRoots": ["./node_modules/@types"]
}
"extends": "./.config/tsconfig.json"
}

14341
yarn.lock

File diff suppressed because it is too large Load Diff