Compare commits

..

No commits in common. "master" and "v2.2.0" have entirely different histories.

58 changed files with 15207 additions and 12724 deletions

View File

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

View File

@ -1,25 +0,0 @@
/*
* ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/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"
}
}
]
}

View File

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

View File

@ -1,16 +0,0 @@
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

View File

@ -1,164 +0,0 @@
# 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.
---

View File

@ -1,25 +0,0 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/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 = () => {};

View File

@ -1,43 +0,0 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/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

@ -1,25 +0,0 @@
// 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;

View File

@ -1,31 +0,0 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in .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,
};

View File

@ -1,26 +0,0 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/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"
}

View File

@ -1,37 +0,0 @@
// 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

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

View File

@ -1,58 +0,0 @@
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

@ -1,214 +0,0 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/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;

View File

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

View File

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

View File

@ -4,90 +4,72 @@ on:
push:
branches:
- master
- main
pull_request:
branches:
- master
- main
jobs:
build:
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
- uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v3
uses: actions/setup-node@v2.1.2
with:
node-version: '20'
cache: 'pnpm'
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-
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline
run: yarn install --frozen-lockfile
- 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: Build and test frontend
run: yarn build
- name: Check for backend
id: check-for-backend
run: |
if [ -f "Magefile.go" ]
then
echo "has-backend=true" >> $GITHUB_OUTPUT
echo "::set-output name=has-backend::true"
fi
- name: Setup Go environment
if: steps.check-for-backend.outputs.has-backend == 'true'
uses: actions/setup-go@v3
uses: actions/setup-go@v2
with:
go-version: '1.21'
go-version: "1.15"
- name: Test backend
if: steps.check-for-backend.outputs.has-backend == 'true'
uses: magefile/mage-action@v2
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@v2
uses: magefile/mage-action@v1
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

View File

@ -1,22 +0,0 @@
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,19 +3,162 @@ 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@v3
- uses: pnpm/action-setup@v2
- uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.2
with:
version: 8
- uses: grafana/plugin-actions/build-plugin@release
# uncomment to enable plugin sign in
node-version: "14.x"
- name: Setup Go environment
uses: actions/setup-go@v2
with:
# 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 }}
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 .

5
.gitignore vendored
View File

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

View File

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

17
.npmrc
View File

@ -1,17 +0,0 @@
# 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
View File

@ -1 +0,0 @@
20

View File

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

View File

@ -5,36 +5,6 @@ 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
@ -54,7 +24,7 @@ Ready for submission to grafana official plugins repository. 🥰
### 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
@ -68,6 +38,6 @@ Ready for submission to grafana official plugins repository. 🥰
### Added
- 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
- 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/)

116
README.md
View File

@ -2,41 +2,38 @@
Grafana data source plugin for Chaos Mesh.
> 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.
> Require: Chaos Mesh >= **2.1.0**, Grafana >= **7.0.0**
## Features
- 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
- 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/)
## 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
<!-- ## Install
```sh
grafana-cli plugins install chaosmeshorg-datasource
```
``` -->
## Manual installation
## 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.
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/v3.0.0/chaosmeshorg-datasource-3.0.0.zip
curl -LO https://github.com/chaos-mesh/datasource/releases/download/v2.2.0/chaosmeshorg-datasource-2.2.0.zip
```
After downloading, unzip:
```shell
unzip chaosmeshorg-datasource-3.0.0.zip -d YOUR_PLUGIN_DIR
unzip chaosmeshorg-datasource-2.2.0.zip -d YOUR_PLUGIN_DIR
```
Then update and save the `grafana.ini` file:
@ -50,35 +47,23 @@ Finally, restart Grafana to load the plugin.
## Setup
Once installed, go to **Administration -> Data sources** and add Chaos Mesh, then go to the configuration page:
Once installed, go to **Configuration -> 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, 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`.
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`.
Then use the `port-forward` command to make the API externally accessible:
Then use the `port-forward` command to activate:
```shell
kubectl port-forward -n chaos-mesh svc/chaos-dashboard 2333:2333
kubectl port-forward -n chaos-testing svc/chaos-dashboard 2333:2333
```
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.
Finally, click **Save & Test** to test the connection. If it shows a successful notification, the setup is complete.
## 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**
@ -94,43 +79,48 @@ The data source plugin looks at the Chaos Mesh through the lens of events, and t
- **Kind**
> Filter by kind (PodChaos, NetworkChaos, Schedule...). You can also input an arbitrary kind
> if you implement a new kind in Chaos Mesh.
> Filter by kind (PodChaos, Schedule...).
- **Limit**
> Limit the number of events.
All of them will be passed as parameters to the `/api/events` API.
## Variables
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**
> After selection, all available namespaces will show in the **Preview of values** directly.
- **Kind**
> Same as **Namespace**. Retrieve all kinds.
- **Experiment/Schedule/Workflow**
> 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.
They will be passed as parameters to the `/api/events` API.
## Annotations
You can integrate events into panels via annotations, the following is a sample creation, it will retrieve all PodChaos events:
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 [Query](#query) to fill in the corresponding fields.
Please refer to the contents of [Query](#query) to fill in the corresponding fields.
## Variables
If you choose the type to `Query` and select the data source to `Chaos Mesh`, you can retrieve
the variables by different metrics:
![Variables](https://raw.githubusercontent.com/chaos-mesh/datasource/master/src/img/variables.png)
- Namespace
> After selection, all available namespaces will show in **Preview of values** directly. Without other operations.
- Kind
> Same as **Namespace**. Retrieve all kinds.
- Experiment
> Same as **Namespace**. Retrieve current all experiments.
- Schedule
> Same as **Namespace**. Retrieve current all schedules.
- Workflow
> Same as **Namespace**. Retrieve current all workflows.
## How to contribute
@ -138,4 +128,4 @@ Pull a request or open an issue to describe your changes or problems.
## License
Same as Chaos Mesh. Under the Apache-2.0 License.
Same as Chaos Mesh. Under Apache-2.0 License.

View File

@ -1,46 +0,0 @@
# 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>.

20
bundle.sh Executable file
View File

@ -0,0 +1,20 @@
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."

View File

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

View File

@ -1,10 +0,0 @@
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');
},
});

View File

@ -1,16 +0,0 @@
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

View File

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

View File

@ -1,8 +1,8 @@
// 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';
// 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
module.exports = {
// Jest configuration provided by Grafana scaffolding
...require('./.config/jest.config'),
};
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();

View File

@ -1,82 +1,36 @@
{
"name": "chaosmeshorg-datasource",
"version": "3.0.0",
"description": "Grafana data source plugin for Chaos Mesh (A Chaos Engineering Platform for Kubernetes)",
"version": "2.2.0",
"description": "Chaos Mesh (A Chaos Engineering Platform for Kubernetes) Data source",
"scripts": {
"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",
"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",
"prepare": "husky install"
},
"author": "Yue Yang <g1enyy0ung@gmail.com>",
"license": "Apache-2.0",
"devDependencies": {
"@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"
"@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"
},
"engines": {
"node": ">=20"
"node": ">=14"
},
"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"
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,8 +0,0 @@
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

@ -0,0 +1,32 @@
/*
* 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

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

80
src/ChaosMeshSettings.tsx Normal file
View File

@ -0,0 +1,80 @@
/*
* 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),
},
});
};

41
src/ConfigEditor.tsx Normal file
View File

@ -0,0 +1,41 @@
/*
* 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} />
</>
);
}
}

81
src/QueryEditor.tsx Normal file
View File

@ -0,0 +1,81 @@
/*
* 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

@ -0,0 +1,93 @@
/*
* 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

@ -1,77 +0,0 @@
/*
* 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

@ -1,105 +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 { 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,240 +16,188 @@
*/
import {
AnnotationEvent,
AnnotationQuery,
AnnotationSupport,
DataFrame,
AnnotationQueryRequest,
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
FieldType,
MutableDataFrame,
} from '@grafana/data'
import {
BackendSrvRequest,
getBackendSrv,
isFetchError,
} from '@grafana/runtime'
import _ from 'lodash'
import { lastValueFrom, of } from 'rxjs'
import { processMultipleVariables, processVariables } from 'utils'
ScopedVars,
} from '@grafana/data';
import { getBackendSrv, getTemplateSrv } from '@grafana/runtime';
import _ from 'lodash';
import { lastValueFrom } from 'rxjs';
import {
ChaosMeshOptions,
ChaosMeshVariableQuery,
Event,
EventQuery,
kinds,
} from './types'
import { ChaosMeshOptions, Event, EventsQuery, VariableQuery, defaultQuery, kinds } from './types';
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' },
},
]
const timeformat = 'YYYY-MM-DDTHH:mm:ssZ';
export class DataSource extends DataSourceApi<EventsQuery, ChaosMeshOptions> {
readonly url?: string;
readonly defaultQuery = defaultQuery;
readonly legacyFetch: boolean = false;
constructor(instanceSettings: DataSourceInstanceSettings<ChaosMeshOptions>) {
super(instanceSettings)
super(instanceSettings);
this.baseUrl = instanceSettings.url + '/api'
this.url = instanceSettings.url + '/api';
if (instanceSettings.jsonData.limit) {
this.defaultQuery.limit = instanceSettings.jsonData.limit;
}
if (typeof getBackendSrv().fetch !== 'function') {
this.legacyFetch = true;
}
}
private fetch<T>(options: BackendSrvRequest) {
const _options = _.defaults(
{
url: this.baseUrl + options.url,
},
options,
{
method: 'GET',
}
)
private fetch<T>(url: string, query?: Partial<EventsQuery>) {
const options = {
method: 'GET',
url: this.url + url,
params: _.defaults(query, this.defaultQuery),
};
const resp = getBackendSrv().fetch<T>(_options)
return lastValueFrom(resp)
return (
!this.legacyFetch
? lastValueFrom(getBackendSrv().fetch<T>(options))
: getBackendSrv().datasourceRequest<T>(options)
).then(({ data }) => data);
}
private fetchEvents(query: Partial<EventQuery>) {
return this.fetch<Event[]>({
url: '/events',
params: query,
})
private fetchEvents(query: Partial<EventsQuery>) {
return this.fetch<Event[]>('/events', query);
}
async query(
options: DataQueryRequest<EventQuery>
): Promise<DataQueryResponse> {
const { range, scopedVars } = options
const from = range.from.toISOString()
const to = range.to.toISOString()
/**
* 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);
return Promise.all(
options.targets.map(async (query) => {
processVariables(query, scopedVars)
options.targets.map(async query => {
query.start = from;
query.end = to;
query.start = from
query.end = to
const queries = processMultipleVariables(query)
const frame = new MutableDataFrame<Event>({
refId: query.refId,
fields: this.fields,
})
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 },
],
});
for (const q of queries) {
;(await this.fetchEvents(q)).data.forEach((d) => frame.add(d))
}
(await this.fetchEvents(this.applyVariables(query, scopedVars))).forEach(d => frame.add(d));
return frame
return frame;
})
).then((data) => ({ data }))
).then(data => ({ data }));
}
async testDatasource() {
const defaultErrorMessage = 'Cannot connect to API'
try {
const resp = await this.fetch({
url: '/common/config',
})
if (resp.status === 200) {
return getBackendSrv()
.get(this.url + '/common/config')
.then(() => {
return {
status: 'success',
message: 'Chaos Mesh API is available',
}
} else {
message: 'Chaos Mesh API status is normal.',
};
})
.catch(error => {
const { data } = error;
return {
status: 'error',
message:
`Status code: ${resp.status}.` +
' Chaos Mesh API is not available.',
}
message: data ? data.message : error.statusText ? error.statusText : '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;
}
} 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,
const val = (query as any)[q];
if (typeof val === 'string' && val.startsWith('$') && vars[val]) {
(query as any)[q] = vars[val];
}
}
query.start = from;
query.end = to;
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];
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),
};
});
});
}
annotations: AnnotationSupport<EventQuery> = {
processEvents: (anno, data) => {
return of(this.eventFrameToAnnotation(anno, data))
},
}
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: `<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: ChaosMeshVariableQuery) {
async metricFindQuery(query: VariableQuery) {
switch (query.metric) {
case 'namespace':
return this.fetch<string[]>({
url: '/common/chaos-available-namespaces',
}).then(({ data }) => data.map((d) => ({ text: d })))
return this.fetch<string[]>('/common/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 }>>({
url: `/${query.metric}s${query.queryString || ''}`,
}).then(({ data }) => data.map((d) => ({ text: d.name })))
return this.fetch<Array<{ name: string }>>(`/${query.metric}s${query.queryString || ''}`).then(data =>
data.map(d => ({ text: d.name }))
);
default:
return []
return [];
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 184 KiB

View File

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

View File

@ -0,0 +1,30 @@
<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/main/docs/sources/developers/plugins/plugin.schema.json",
"$schema": "https://raw.githubusercontent.com/grafana/grafana/master/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": "Grafana data source plugin for Chaos Mesh (A Chaos Engineering Platform for Kubernetes)",
"description": "Chaos Mesh (A Chaos Engineering Platform for Kubernetes) Datasource",
"author": {
"name": "Chaos Mesh Project Team",
"url": "https://github.com/chaos-mesh"
},
"keywords": ["datasource", "Cloud", "Chaos Engineering", "Chaos Mesh"],
"keywords": ["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": "Variables",
"path": "img/variables.png"
},
{
"name": "Annotations",
"path": "img/annotations.png"
},
{
"name": "Variables",
"path": "img/variables.png"
}
],
"version": "%VERSION%",
"version": "2.2.0",
"updated": "%TODAY%"
},
"dependencies": {
"grafanaDependency": ">=10.0.3",
"grafanaDependency": ">=7.0.0",
"plugins": []
}
}

View File

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

View File

@ -1,85 +0,0 @@
/*
* 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,3 +1,9 @@
{
"extends": "./.config/tsconfig.json"
"extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json",
"include": ["src", "types"],
"compilerOptions": {
"rootDir": "./src",
"baseUrl": "./src",
"typeRoots": ["./node_modules/@types"]
}
}

14341
yarn.lock Normal file

File diff suppressed because it is too large Load Diff