Compare commits
25 Commits
Author | SHA1 | Date |
---|---|---|
|
91a1f3fa41 | |
|
ce64162aff | |
|
b12d852975 | |
|
55cd414baa | |
|
c708e2760b | |
|
f0c1e6c014 | |
|
5b31696bb7 | |
|
604a2b42a8 | |
|
efdcb7015f | |
|
9f60dca6b4 | |
|
751aadcf89 | |
|
19570eb209 | |
|
53d5eb9236 | |
|
c7192de2bd | |
|
b9e9aad715 | |
|
1eb3ef25fc | |
|
86404dbd65 | |
|
0691befc9c | |
|
ebcbb6d580 | |
|
c56081a00f | |
|
d7ec01506a | |
|
874a4601ce | |
|
46fb45d1e0 | |
|
6c2e2c2b94 | |
|
e2c8bb38c1 |
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"version": "2.10.1"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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
|
|
@ -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.
|
||||
|
||||
---
|
|
@ -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 = () => {};
|
|
@ -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)],
|
||||
};
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
|
@ -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"
|
||||
}
|
|
@ -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';
|
|
@ -0,0 +1,2 @@
|
|||
export const SOURCE_DIR = 'src';
|
||||
export const DIST_DIR = 'dist';
|
|
@ -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);
|
||||
}, {});
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"features": {}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
|
|
|
@ -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,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'],
|
||||
}
|
||||
|
|
38
CHANGELOG.md
38
CHANGELOG.md
|
@ -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
|
||||
|
|
96
README.md
96
README.md
|
@ -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:
|
||||
|
||||

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

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

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

|
||||
|
||||
> 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.
|
||||
|
|
|
@ -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>.
|
20
bundle.sh
20
bundle.sh
|
@ -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."
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"video": false
|
||||
}
|
|
@ -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');
|
||||
},
|
||||
});
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
// Jest setup provided by Grafana scaffolding
|
||||
import './.config/jest-setup';
|
|
@ -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'),
|
||||
};
|
||||
|
|
90
package.json
90
package.json
|
@ -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"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
For more information see [Provision dashboards and data sources](https://grafana.com/tutorials/provision-dashboards-and-data-sources/)
|
|
@ -0,0 +1,8 @@
|
|||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: 'Chaos Mesh'
|
||||
type: 'chaosmeshorg-datasource'
|
||||
access: proxy
|
||||
isDefault: true
|
||||
editable: true
|
|
@ -1 +1 @@
|
|||
type uuid = string;
|
||||
type uuid = string
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
if (typeof getBackendSrv().fetch !== 'function') {
|
||||
this.legacyFetch = true;
|
||||
}
|
||||
this.baseUrl = instanceSettings.url + '/api'
|
||||
}
|
||||
|
||||
private fetch<T>(url: string, query?: Partial<EventsQuery>) {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: this.url + url,
|
||||
params: _.defaults(query, this.defaultQuery),
|
||||
};
|
||||
private fetch<T>(options: BackendSrvRequest) {
|
||||
const _options = _.defaults(
|
||||
{
|
||||
url: this.baseUrl + options.url,
|
||||
},
|
||||
options,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
)
|
||||
|
||||
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 },
|
||||
],
|
||||
});
|
||||
fields: this.fields,
|
||||
})
|
||||
|
||||
(await this.fetchEvents(this.applyVariables(query, scopedVars))).forEach(d => frame.add(d));
|
||||
for (const q of queries) {
|
||||
;(await this.fetchEvents(q)).data.forEach((d) => frame.add(d))
|
||||
}
|
||||
|
||||
return frame;
|
||||
return frame
|
||||
})
|
||||
).then(data => ({ data }));
|
||||
).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.',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
message:
|
||||
`Status code: ${resp.status}.` +
|
||||
' Chaos Mesh API is not available.',
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async metricFindQuery(query: VariableQuery) {
|
||||
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) {
|
||||
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 |
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
46
src/types.ts
46
src/types.ts
|
@ -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 {}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue