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:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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
|
- name: Setup Node.js environment
|
||||||
uses: actions/setup-node@v2.1.2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "14.x"
|
node-version: '20'
|
||||||
|
cache: 'pnpm'
|
||||||
- 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
|
- name: Install dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: pnpm install --frozen-lockfile --prefer-offline
|
||||||
|
|
||||||
- name: Build and test frontend
|
- name: Check types
|
||||||
run: yarn build
|
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
|
- name: Check for backend
|
||||||
id: check-for-backend
|
id: check-for-backend
|
||||||
run: |
|
run: |
|
||||||
if [ -f "Magefile.go" ]
|
if [ -f "Magefile.go" ]
|
||||||
then
|
then
|
||||||
echo "::set-output name=has-backend::true"
|
echo "has-backend=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup Go environment
|
- name: Setup Go environment
|
||||||
if: steps.check-for-backend.outputs.has-backend == 'true'
|
if: steps.check-for-backend.outputs.has-backend == 'true'
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "1.15"
|
go-version: '1.21'
|
||||||
|
|
||||||
- name: Test backend
|
- name: Test backend
|
||||||
if: steps.check-for-backend.outputs.has-backend == 'true'
|
if: steps.check-for-backend.outputs.has-backend == 'true'
|
||||||
uses: magefile/mage-action@v1
|
uses: magefile/mage-action@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: coverage
|
args: coverage
|
||||||
|
|
||||||
- name: Build backend
|
- name: Build backend
|
||||||
if: steps.check-for-backend.outputs.has-backend == 'true'
|
if: steps.check-for-backend.outputs.has-backend == 'true'
|
||||||
uses: magefile/mage-action@v1
|
uses: magefile/mage-action@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: buildAll
|
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:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*.*.*" # Run workflow on version tags, e.g. v1.0.0.
|
- 'v*' # Run workflow on version tags, e.g. v1.0.0.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
- name: Setup Node.js environment
|
|
||||||
uses: actions/setup-node@v2.1.2
|
|
||||||
with:
|
with:
|
||||||
node-version: "14.x"
|
version: 8
|
||||||
|
- uses: grafana/plugin-actions/build-plugin@release
|
||||||
- name: Setup Go environment
|
# uncomment to enable plugin sign in
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
with:
|
||||||
go-version: "1.15"
|
# 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
|
||||||
- name: Get yarn cache directory path
|
policy_token: ${{ secrets.POLICY_TOKEN }}
|
||||||
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 .
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ logs
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
@ -25,10 +26,14 @@ artifacts/
|
||||||
work/
|
work/
|
||||||
ci/
|
ci/
|
||||||
e2e-results/
|
e2e-results/
|
||||||
|
**/cypress/videos
|
||||||
|
**/cypress/report.json
|
||||||
|
|
||||||
# Editor
|
# Editor
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
# npm
|
# npm
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
npx lint-staged
|
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 = {
|
module.exports = {
|
||||||
...require('./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json'),
|
// Prettier configuration provided by Grafana scaffolding
|
||||||
arrowParens: 'avoid',
|
...require('./.config/.prettierrc.js'),
|
||||||
importOrder: [
|
semi: false,
|
||||||
'<THIRD_PARTY_MODULES>',
|
printWidth: 80,
|
||||||
'^@ui/(.*)$',
|
importOrder: ['<THIRD_PARTY_MODULES>', '^[./]'],
|
||||||
'store',
|
|
||||||
'^slices/(.*)$',
|
|
||||||
'^components/(.*)$',
|
|
||||||
'^lib/(.*)$',
|
|
||||||
'^images/(.*)$',
|
|
||||||
'^[./]',
|
|
||||||
],
|
|
||||||
importOrderSeparation: true,
|
importOrderSeparation: true,
|
||||||
importOrderSortSpecifiers: 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/),
|
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).
|
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
|
## [2.2.0] - 2022-06-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -24,7 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Changed
|
### 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 the minimal grafana version to 7.0.0
|
||||||
- Bump grafana/toolkit to 8.x
|
- 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
|
### Added
|
||||||
|
|
||||||
- Visualize Chaos Events on the table
|
- Displaying Chaos events in a table visualization
|
||||||
- Show Chaos Events on the graph with [Annotations](https://grafana.com/docs/grafana/latest/dashboards/annotations/)
|
- Support [Variables](https://grafana.com/docs/grafana/latest/variables/) to filter Chaos events
|
||||||
- Display different Chaos Events by [Variables](https://grafana.com/docs/grafana/latest/variables/)
|
- 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.
|
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
|
## Features
|
||||||
|
|
||||||
- Visualize Chaos Events on the table
|
- Displaying Chaos events in a table visualization
|
||||||
- Show Chaos Events on the graph with [Annotations](https://grafana.com/docs/grafana/latest/dashboards/annotations/)
|
- Support [Variables](https://grafana.com/docs/grafana/latest/variables/) to filter Chaos events
|
||||||
- Display different Chaos Events by [Variables](https://grafana.com/docs/grafana/latest/variables/)
|
- 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
|
```sh
|
||||||
grafana-cli plugins install chaosmeshorg-datasource
|
grafana-cli plugins install chaosmeshorg-datasource
|
||||||
``` -->
|
```
|
||||||
|
|
||||||
## Install
|
## Manual installation
|
||||||
|
|
||||||
> **Note:**
|
|
||||||
>
|
|
||||||
> Because Grafana is not yet accepting the plugin submission for Chaos Mesh Data Source, it can't be installed using `grafana-cli` at this time.
|
|
||||||
>
|
|
||||||
> The following steps show how to install the Data Source plugin locally.
|
|
||||||
|
|
||||||
Download the plugin zip package with the following command or go to <https://github.com/chaos-mesh/datasource/releases> to download:
|
Download the plugin zip package with the following command or go to <https://github.com/chaos-mesh/datasource/releases> to download:
|
||||||
|
|
||||||
```shell
|
```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:
|
After downloading, unzip:
|
||||||
|
|
||||||
```shell
|
```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:
|
Then update and save the `grafana.ini` file:
|
||||||
|
@ -47,23 +50,35 @@ Finally, restart Grafana to load the plugin.
|
||||||
|
|
||||||
## Setup
|
## 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
|
```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
|
## 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**
|
- **Object ID**
|
||||||
|
|
||||||
|
@ -79,48 +94,43 @@ The Data Source plugin looks at the Chaos Mesh through the lens of events, and t
|
||||||
|
|
||||||
- **Kind**
|
- **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**
|
||||||
|
|
||||||
> Limit the number of events.
|
> Limit the number of events.
|
||||||
|
|
||||||
They will be passed as parameters to the `/api/events` API.
|
All of them 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.
|
|
||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
|
|
||||||
If you choose the type to `Query` and select the data source to `Chaos Mesh`, you can retrieve
|
The data source plugin supports adding query variables by different metrics:
|
||||||
the 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.
|
> 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
|
## How to contribute
|
||||||
|
|
||||||
|
@ -128,4 +138,4 @@ Pull a request or open an issue to describe your changes or problems.
|
||||||
|
|
||||||
## License
|
## 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
|
// force timezone to UTC to allow tests to work regardless of local timezone
|
||||||
// call `jest` directly. However, unless you are doing anything special
|
// generally used by snapshots, but can affect specific tests
|
||||||
// do not edit this file
|
process.env.TZ = 'UTC';
|
||||||
|
|
||||||
const standard = require('@grafana/toolkit/src/config/jest.plugin.config');
|
module.exports = {
|
||||||
|
// Jest configuration provided by Grafana scaffolding
|
||||||
// This process will use the same config that `yarn test` is using
|
...require('./.config/jest.config'),
|
||||||
module.exports = standard.jestConfig();
|
};
|
||||||
|
|
90
package.json
90
package.json
|
@ -1,36 +1,82 @@
|
||||||
{
|
{
|
||||||
"name": "chaosmeshorg-datasource",
|
"name": "chaosmeshorg-datasource",
|
||||||
"version": "2.2.0",
|
"version": "3.0.0",
|
||||||
"description": "Chaos Mesh (A Chaos Engineering Platform for Kubernetes) Data source",
|
"description": "Grafana data source plugin for Chaos Mesh (A Chaos Engineering Platform for Kubernetes)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "grafana-toolkit plugin:build",
|
"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
|
||||||
"test": "grafana-toolkit plugin:test",
|
"dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development",
|
||||||
"dev": "grafana-toolkit plugin:dev",
|
"test": "jest --watch --onlyChanged",
|
||||||
"watch": "grafana-toolkit plugin:dev --watch",
|
"test:ci": "jest --passWithNoTests --maxWorkers 4",
|
||||||
"sign": "grafana-toolkit plugin:sign",
|
"typecheck": "tsc --noEmit",
|
||||||
"start": "yarn watch",
|
"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"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"author": "Yue Yang <g1enyy0ung@gmail.com>",
|
"author": "Yue Yang <g1enyy0ung@gmail.com>",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@grafana/data": "7.0.x",
|
"@babel/core": "^7.21.4",
|
||||||
"@grafana/runtime": "^8.2.2",
|
"@grafana/e2e": "10.0.3",
|
||||||
"@grafana/toolkit": "^8.2.2",
|
"@grafana/e2e-selectors": "10.0.3",
|
||||||
"@grafana/ui": "7.0.x",
|
"@grafana/eslint-config": "^6.0.0",
|
||||||
"@testing-library/jest-dom": "5.4.0",
|
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||||
"@testing-library/react": "^10.0.2",
|
"@swc/core": "^1.3.90",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
|
"@swc/helpers": "^0.5.0",
|
||||||
"@types/lodash": "^4.14.176",
|
"@swc/jest": "^0.2.26",
|
||||||
"husky": "^7.0.4",
|
"@testing-library/jest-dom": "6.1.4",
|
||||||
"lint-staged": "^11.2.4",
|
"@testing-library/react": "14.0.0",
|
||||||
"prettier": "^2.4.1",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
"react": "16.12.0",
|
"@types/jest": "^29.5.0",
|
||||||
"react-dom": "16.12.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": {
|
"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": {
|
"lint-staged": {
|
||||||
"*.ts?(x)": "prettier --write"
|
"*.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.
|
* limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import '@testing-library/jest-dom';
|
import { DataSourcePluginOptionsEditorProps } from '@grafana/data'
|
||||||
import { render, screen } from '@testing-library/react';
|
import { DataSourceHttpSettings } from '@grafana/ui'
|
||||||
import { ChaosMeshSettings } from 'ChaosMeshSettings';
|
import React from 'react'
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const defaultOptions = {
|
import { ChaosMeshOptions } from '../types'
|
||||||
jsonData: {
|
|
||||||
limit: 300,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('<ChaosMeshSettings />', () => {
|
export function ConfigEditor({
|
||||||
it('loaded successfully', () => {
|
options,
|
||||||
render(<ChaosMeshSettings options={defaultOptions as any} onOptionsChange={() => {}} />);
|
onOptionsChange,
|
||||||
|
}: DataSourcePluginOptionsEditorProps<ChaosMeshOptions>) {
|
||||||
expect(screen.getByText('Limit')).toBeInTheDocument();
|
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 {
|
import {
|
||||||
AnnotationEvent,
|
AnnotationEvent,
|
||||||
AnnotationQueryRequest,
|
AnnotationQuery,
|
||||||
|
AnnotationSupport,
|
||||||
|
DataFrame,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
FieldType,
|
FieldType,
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
ScopedVars,
|
} from '@grafana/data'
|
||||||
} from '@grafana/data';
|
import {
|
||||||
import { getBackendSrv, getTemplateSrv } from '@grafana/runtime';
|
BackendSrvRequest,
|
||||||
import _ from 'lodash';
|
getBackendSrv,
|
||||||
import { lastValueFrom } from 'rxjs';
|
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<EventQuery, ChaosMeshOptions> {
|
||||||
|
readonly baseUrl: string
|
||||||
export class DataSource extends DataSourceApi<EventsQuery, ChaosMeshOptions> {
|
readonly fields = [
|
||||||
readonly url?: string;
|
{
|
||||||
readonly defaultQuery = defaultQuery;
|
name: 'object_id',
|
||||||
readonly legacyFetch: boolean = false;
|
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>) {
|
constructor(instanceSettings: DataSourceInstanceSettings<ChaosMeshOptions>) {
|
||||||
super(instanceSettings);
|
super(instanceSettings)
|
||||||
|
|
||||||
this.url = instanceSettings.url + '/api';
|
this.baseUrl = instanceSettings.url + '/api'
|
||||||
|
|
||||||
if (instanceSettings.jsonData.limit) {
|
|
||||||
this.defaultQuery.limit = instanceSettings.jsonData.limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof getBackendSrv().fetch !== 'function') {
|
|
||||||
this.legacyFetch = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetch<T>(url: string, query?: Partial<EventsQuery>) {
|
private fetch<T>(options: BackendSrvRequest) {
|
||||||
const options = {
|
const _options = _.defaults(
|
||||||
method: 'GET',
|
{
|
||||||
url: this.url + url,
|
url: this.baseUrl + options.url,
|
||||||
params: _.defaults(query, this.defaultQuery),
|
},
|
||||||
};
|
options,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
const resp = getBackendSrv().fetch<T>(_options)
|
||||||
!this.legacyFetch
|
|
||||||
? lastValueFrom(getBackendSrv().fetch<T>(options))
|
return lastValueFrom(resp)
|
||||||
: getBackendSrv().datasourceRequest<T>(options)
|
|
||||||
).then(({ data }) => data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchEvents(query: Partial<EventsQuery>) {
|
private fetchEvents(query: Partial<EventQuery>) {
|
||||||
return this.fetch<Event[]>('/events', query);
|
return this.fetch<Event[]>({
|
||||||
|
url: '/events',
|
||||||
|
params: query,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async query(
|
||||||
* This function use a tricky way to apply all the variables to the query.
|
options: DataQueryRequest<EventQuery>
|
||||||
*
|
): Promise<DataQueryResponse> {
|
||||||
* It firstly joins all values to a string and then replace by scoped vars, then zip the result to a object.
|
const { range, scopedVars } = options
|
||||||
*
|
const from = range.from.toISOString()
|
||||||
* @param query
|
const to = range.to.toISOString()
|
||||||
* @param scopedVars
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
private applyVariables(query: EventsQuery, scopedVars: ScopedVars) {
|
|
||||||
const keys = _.keys(query);
|
|
||||||
const values = getTemplateSrv().replace(_.values(query).join('|'), scopedVars).split('|');
|
|
||||||
|
|
||||||
return _.zipObject(keys, values) as unknown as EventsQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
async query(options: DataQueryRequest<EventsQuery>): Promise<DataQueryResponse> {
|
|
||||||
const { range, scopedVars } = options;
|
|
||||||
|
|
||||||
const from = range.from.utc().format(timeformat);
|
|
||||||
const to = range.to.utc().format(timeformat);
|
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
options.targets.map(async query => {
|
options.targets.map(async (query) => {
|
||||||
query.start = from;
|
processVariables(query, scopedVars)
|
||||||
query.end = to;
|
|
||||||
|
|
||||||
|
query.start = from
|
||||||
|
query.end = to
|
||||||
|
|
||||||
|
const queries = processMultipleVariables(query)
|
||||||
const frame = new MutableDataFrame<Event>({
|
const frame = new MutableDataFrame<Event>({
|
||||||
refId: query.refId,
|
refId: query.refId,
|
||||||
fields: [
|
fields: this.fields,
|
||||||
{ name: 'object_id', type: FieldType.string },
|
})
|
||||||
{ name: 'namespace', type: FieldType.string },
|
|
||||||
{ name: 'name', type: FieldType.string },
|
|
||||||
{ name: 'kind', type: FieldType.string },
|
|
||||||
{ name: 'created_at', type: FieldType.time },
|
|
||||||
{ name: 'type', type: FieldType.string },
|
|
||||||
{ name: 'reason', type: FieldType.string },
|
|
||||||
{ name: 'message', type: FieldType.string },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
(await this.fetchEvents(this.applyVariables(query, scopedVars))).forEach(d => frame.add(d));
|
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() {
|
async testDatasource() {
|
||||||
return getBackendSrv()
|
const defaultErrorMessage = 'Cannot connect to API'
|
||||||
.get(this.url + '/common/config')
|
|
||||||
.then(() => {
|
try {
|
||||||
|
const resp = await this.fetch({
|
||||||
|
url: '/common/config',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (resp.status === 200) {
|
||||||
return {
|
return {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Chaos Mesh API status is normal.',
|
message: 'Chaos Mesh API is available',
|
||||||
};
|
}
|
||||||
})
|
} else {
|
||||||
.catch(error => {
|
|
||||||
const { data } = error;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: data ? data.message : error.statusText ? error.statusText : 'Chaos Mesh API is not available.',
|
message:
|
||||||
};
|
`Status code: ${resp.status}.` +
|
||||||
});
|
' Chaos Mesh API is not available.',
|
||||||
}
|
}
|
||||||
|
|
||||||
async annotationQuery(options: AnnotationQueryRequest<EventsQuery>): Promise<AnnotationEvent[]> {
|
|
||||||
const { range, annotation: query } = options;
|
|
||||||
|
|
||||||
const from = range.from.utc().format(timeformat);
|
|
||||||
const to = range.to.utc().format(timeformat);
|
|
||||||
|
|
||||||
const vars = _.mapValues(
|
|
||||||
_.keyBy(getTemplateSrv().getVariables(), d => '$' + d.name),
|
|
||||||
'current.value'
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const q in query) {
|
|
||||||
if (!query.hasOwnProperty(q)) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
const val = (query as any)[q];
|
let message = ''
|
||||||
|
if (_.isString(err)) {
|
||||||
if (typeof val === 'string' && val.startsWith('$') && vars[val]) {
|
message = err
|
||||||
(query as any)[q] = vars[val];
|
} 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) {
|
switch (query.metric) {
|
||||||
case 'namespace':
|
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':
|
case 'kind':
|
||||||
return kinds.map(d => ({ text: d }));
|
return kinds.map((d) => ({ text: d }))
|
||||||
case 'experiment':
|
case 'experiment':
|
||||||
case 'schedule':
|
case 'schedule':
|
||||||
case 'workflow':
|
case 'workflow':
|
||||||
return this.fetch<Array<{ name: string }>>(`/${query.metric}s${query.queryString || ''}`).then(data =>
|
return this.fetch<Array<{ name: string }>>({
|
||||||
data.map(d => ({ text: d.name }))
|
url: `/${query.metric}s${query.queryString || ''}`,
|
||||||
);
|
}).then(({ data }) => data.map((d) => ({ text: d.name })))
|
||||||
default:
|
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.
|
* limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import { DataSourcePlugin } from '@grafana/data';
|
import { DataSourcePlugin } from '@grafana/data'
|
||||||
import { AnnotationQueryEditor } from 'AnnotationQueryEditor';
|
|
||||||
|
|
||||||
import { ConfigEditor } from './ConfigEditor';
|
import { ConfigEditor } from './components/ConfigEditor'
|
||||||
import { QueryEditor } from './QueryEditor';
|
import { QueryEditor } from './components/QueryEditor'
|
||||||
import { VariableQueryEditor } from './VariableQueryEditor';
|
import { VariableQueryEditor } from './components/VariableQueryEditor'
|
||||||
import { DataSource } from './datasource';
|
import { DataSource } from './datasource'
|
||||||
import { ChaosMeshOptions, EventsQuery } from './types';
|
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)
|
.setConfigEditor(ConfigEditor)
|
||||||
.setQueryEditor(QueryEditor)
|
.setQueryEditor(QueryEditor)
|
||||||
.setVariableQueryEditor(VariableQueryEditor)
|
.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",
|
"type": "datasource",
|
||||||
"name": "Chaos Mesh",
|
"name": "Chaos Mesh",
|
||||||
"id": "chaosmeshorg-datasource",
|
"id": "chaosmeshorg-datasource",
|
||||||
|
@ -7,12 +7,12 @@
|
||||||
"annotations": true,
|
"annotations": true,
|
||||||
"metrics": true,
|
"metrics": true,
|
||||||
"info": {
|
"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": {
|
"author": {
|
||||||
"name": "Chaos Mesh Project Team",
|
"name": "Chaos Mesh Project Team",
|
||||||
"url": "https://github.com/chaos-mesh"
|
"url": "https://github.com/chaos-mesh"
|
||||||
},
|
},
|
||||||
"keywords": ["Cloud", "Chaos Engineering", "Chaos Mesh"],
|
"keywords": ["datasource", "Cloud", "Chaos Engineering", "Chaos Mesh"],
|
||||||
"logos": {
|
"logos": {
|
||||||
"small": "img/logo.svg",
|
"small": "img/logo.svg",
|
||||||
"large": "img/logo.svg"
|
"large": "img/logo.svg"
|
||||||
|
@ -36,20 +36,20 @@
|
||||||
"name": "Settings",
|
"name": "Settings",
|
||||||
"path": "img/settings.png"
|
"path": "img/settings.png"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Annotations",
|
|
||||||
"path": "img/annotations.png"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Variables",
|
"name": "Variables",
|
||||||
"path": "img/variables.png"
|
"path": "img/variables.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Annotations",
|
||||||
|
"path": "img/annotations.png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version": "2.2.0",
|
"version": "%VERSION%",
|
||||||
"updated": "%TODAY%"
|
"updated": "%TODAY%"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaDependency": ">=7.0.0",
|
"grafanaDependency": ">=10.0.3",
|
||||||
"plugins": []
|
"plugins": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
46
src/types.ts
46
src/types.ts
|
@ -14,32 +14,30 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import { DataQuery, DataSourceJsonData } from '@grafana/data';
|
import { DataSourceJsonData } from '@grafana/data'
|
||||||
|
import { DataQuery } from '@grafana/schema'
|
||||||
|
|
||||||
interface EventBase {
|
interface EventBase {
|
||||||
object_id: uuid;
|
object_id: uuid
|
||||||
namespace: string;
|
namespace: string
|
||||||
name: string;
|
name: string
|
||||||
kind: string;
|
kind: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Event extends EventBase {
|
export interface Event extends EventBase {
|
||||||
created_at: string;
|
created_at: string
|
||||||
type: 'Normal' | 'Warning';
|
type: string
|
||||||
reason: string;
|
reason: string
|
||||||
message: string;
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventsQuery extends DataQuery, EventBase {
|
export interface EventQuery extends DataQuery, EventBase {
|
||||||
start: string;
|
start: string
|
||||||
end: string;
|
end: string
|
||||||
limit?: number;
|
limit?: number
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultQuery: Partial<EventsQuery> = {
|
|
||||||
limit: 300,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const kinds = [
|
export const kinds = [
|
||||||
'AWSChaos',
|
'AWSChaos',
|
||||||
'AzureChaos',
|
'AzureChaos',
|
||||||
|
@ -55,16 +53,16 @@ export const kinds = [
|
||||||
'StressChaos',
|
'StressChaos',
|
||||||
'TimeChaos',
|
'TimeChaos',
|
||||||
'PhysicalMachineChaos',
|
'PhysicalMachineChaos',
|
||||||
];
|
]
|
||||||
|
|
||||||
export interface VariableQuery {
|
export const kindOptions = kinds.map((kind) => ({ label: kind, value: kind }))
|
||||||
metric: 'namespace' | 'kind' | 'experiment' | 'schedule' | 'workflow';
|
|
||||||
queryString?: string;
|
export interface ChaosMeshVariableQuery {
|
||||||
|
metric: 'namespace' | 'kind' | 'experiment' | 'schedule' | 'workflow'
|
||||||
|
queryString?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are options configured for each DataSource instance
|
* These are options configured for each DataSource instance
|
||||||
*/
|
*/
|
||||||
export interface ChaosMeshOptions extends DataSourceJsonData {
|
export interface ChaosMeshOptions extends DataSourceJsonData {}
|
||||||
limit?: number;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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",
|
"extends": "./.config/tsconfig.json"
|
||||||
"include": ["src", "types"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"rootDir": "./src",
|
|
||||||
"baseUrl": "./src",
|
|
||||||
"typeRoots": ["./node_modules/@types"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue