Browse Source

improve ui && fix tests

pull/1/head
rozetko 1 year ago
parent
commit
59a5278679
  1. 17
      .config/jest-setup.js
  2. 48
      .config/jest.config.js
  3. 2
      .config/webpack/constants.ts
  4. 9
      .config/webpack/tsconfig.webpack.json
  5. 42
      .config/webpack/utils.ts
  6. 182
      .config/webpack/webpack.config.ts
  7. 1
      .gitignore
  8. 11
      coverage/junit.xml
  9. 2
      jest-setup.js
  10. 12
      jest.config.js
  11. 47
      package.json
  12. 4
      src/__mocks__/@grafana/runtime.ts
  13. 32
      src/components/App/App.test.tsx
  14. 8
      src/components/App/App.tsx
  15. 1
      src/components/App/index.tsx
  16. 11
      src/components/GBlock/Block.tsx
  17. 2
      src/components/PluginConfigPage/index.tsx
  18. 41
      src/components/PluginConfigPage/parts/ConfigurationForm/index.tsx
  19. 36
      src/components/PluginConfigPage/parts/RemoveCurrentConfigurationButton/__snapshots__/RemoveCurrentConfigurationButton.test.tsx.snap
  20. 12
      src/components/PluginConfigPage/parts/StatusMessageBlock/StatusMessageBlock.test.tsx
  21. 21
      src/components/Text/Text.tsx
  22. 4
      src/img/logo.svg
  23. 6
      src/index.d.ts
  24. 1
      src/jest/grafanaMock.ts
  25. 15
      src/jest/matchMedia.ts
  26. 1
      src/jest/styleMock.ts
  27. 8
      src/jest/svgTransform.ts
  28. 13
      src/jest/utils.ts
  29. 6
      src/module.ts
  30. 10
      src/plugin.json
  31. 45
      src/style/global.css
  32. 78
      src/style/vars.css
  33. 147
      webpack.config.ts
  34. 3560
      yarn.lock

17
.config/jest-setup.js

@ -1,24 +1,21 @@
/*
* 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
/**
* globally import this, avoids needing to import it in each file
* https://stackoverflow.com/a/65871118
*/
import '@testing-library/jest-dom';
// https://stackoverflow.com/a/66055672
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(global, 'matchMedia', {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
HTMLCanvasElement.prototype.getContext = () => {};

48
.config/jest.config.js

@ -1,31 +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 .config/README.md
*/
const esModules = ['react-colorful', 'uuid', 'ol'].join('|');
module.exports = {
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: true,
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
decorators: false,
dynamicImport: true,
},
},
},
],
testEnvironment: 'jsdom',
moduleDirectories: ['node_modules', 'src'],
moduleFileExtensions: ['ts', 'tsx', 'js'],
transformIgnorePatterns: [`/node_modules/(?!${esModules})`],
moduleNameMapper: {
'grafana/app/(.*)': '<rootDir>/src/jest/grafanaMock.ts',
'jest/matchMedia': '<rootDir>/src/jest/matchMedia.ts',
'jest/outgoingWebhooksStub': '<rootDir>/src/jest/outgoingWebhooksStub.ts',
'^jest$': '<rootDir>/src/jest',
'^.+\\.(css|scss)$': '<rootDir>/src/jest/styleMock.ts',
'^lodash-es$': 'lodash',
'^.+\\.svg$': '<rootDir>/src/jest/svgTransform.ts',
'^.+\\.png$': '<rootDir>/src/jest/grafanaMock.ts',
},
setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],
setupFilesAfterEnv: ['<rootDir>/.config/jest.setup.ts'],
testTimeout: 10000,
};

2
.config/webpack/constants.ts

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

9
.config/webpack/tsconfig.webpack.json

@ -1,9 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"esModuleInterop": true
},
"transpileOnly": true,
"transpiler": "ts-node/transpilers/swc-experimental"
}

42
.config/webpack/utils.ts

@ -1,42 +0,0 @@
import fs from 'fs';
import path from 'path';
import util from 'util';
import glob from 'glob';
import { SOURCE_DIR } from './constants';
const globAsync = util.promisify(glob);
export function getPackageJson() {
return require(path.resolve(process.cwd(), 'package.json'));
}
export function getPluginId() {
const { id } = require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`));
return id;
}
export function hasReadme() {
return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md'));
}
export async function getEntries(): Promise<Record<string, string>> {
const parent = '..';
const pluginsJson = await globAsync('**/src/**/plugin.json');
const plugins = await Promise.all(pluginsJson.map(pluginJson => {
const folder = path.dirname(pluginJson);
return globAsync(`${folder}/module.{ts,tsx,js}`);
}));
return plugins.reduce((result, modules) => {
return modules.reduce((result, module) => {
const pluginPath = path.resolve(path.dirname(module), parent);
const pluginName = path.basename(pluginPath);
const entryName = plugins.length > 1 ? `${pluginName}/module` : 'module';
result[entryName] = path.join(parent, module);
return result;
}, result);
}, {});
}

182
.config/webpack/webpack.config.ts

@ -1,182 +0,0 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in .config/README.md
*/
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, getPluginId, hasReadme, getEntries } from './utils';
import { SOURCE_DIR, DIST_DIR } from './constants';
const config = async (env): Promise<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-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)) {
// @ts-ignore
return callback(null, stripPrefix(request));
}
callback();
},
],
mode: env.production ? 'production' : 'development',
module: {
rules: [
{
exclude: /(node_modules)/,
test: /\.[tj]sx?$/,
use: {
loader: 'swc-loader',
options: {
jsc: {
baseUrl: './src',
target: 'es2015',
loose: false,
parser: {
syntax: 'typescript',
tsx: true,
decorators: false,
dynamicImport: true,
},
},
},
},
},
// {
// test: /\.(png|jpe?g|gif|svg)$/,
// type: 'asset/resource',
// options: {
// outputPath: '/',
// name: Boolean(env.production) ? '[path][hash].[ext]' : '[path][name].[ext]',
// },
// },
// {
// test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/,
// type: 'asset/resource',
// options: {
// // Keep publicPath relative for host.com/grafana/ deployments
// publicPath: `public/plugins/${getPluginId()}/fonts`,
// outputPath: 'fonts',
// name: Boolean(env.production) ? '[hash].[ext]' : '[name].[ext]',
// },
// },
],
},
output: {
clean: true,
filename: '[name].js',
libraryTarget: 'amd',
path: path.resolve(process.cwd(), DIST_DIR),
publicPath: '/',
},
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
],
}),
// 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: getPluginId(),
},
],
},
]),
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'],
unsafeCache: true,
},
});
export default config;

1
.gitignore vendored

@ -1,3 +1,4 @@
node_modules
dist
.eslintcache
yarn-error.log

11
coverage/junit.xml

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="jest tests" tests="3" failures="0" errors="0" time="2.424">
<testsuite name="RemoveCurrentConfigurationButton" errors="0" failures="0" skipped="0" timestamp="2022-12-19T18:00:28" time="1.512" tests="3">
<testcase classname="RemoveCurrentConfigurationButton It renders properly when enabled" name="RemoveCurrentConfigurationButton It renders properly when enabled" time="0.022">
</testcase>
<testcase classname="RemoveCurrentConfigurationButton It renders properly when disabled" name="RemoveCurrentConfigurationButton It renders properly when disabled" time="0.003">
</testcase>
<testcase classname="RemoveCurrentConfigurationButton It calls the onClick handler when clicked" name="RemoveCurrentConfigurationButton It calls the onClick handler when clicked" time="0.164">
</testcase>
</testsuite>
</testsuites>

2
jest-setup.js

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

12
jest.config.js

@ -1,4 +1,8 @@
module.exports = {
// Jest configuration provided by Grafana scaffolding
...require('./.config/jest.config'),
};
// This file is needed because it is used by vscode and other tools that
// call `jest` directly. However, unless you are doing anything special
// do not edit this file
const standard = require('@grafana/toolkit/src/config/jest.plugin.config');
// This process will use the same config that `yarn test` is using
module.exports = standard.jestConfig();

47
package.json

@ -3,16 +3,35 @@
"version": "1.0.0",
"description": "",
"scripts": {
"build": "TS_NODE_PROJECT=\"./.config/webpack/tsconfig.webpack.json\" webpack -c ./.config/webpack/webpack.config.ts --env production",
"dev": "TS_NODE_PROJECT=\"./.config/webpack/tsconfig.webpack.json\" webpack -w -c ./.config/webpack/webpack.config.ts --env development",
"test": "jest --watch --onlyChanged",
"test:ci": "jest --maxWorkers 4",
"typecheck": "tsc --noEmit",
"lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .",
"lint:fix": "yarn lint --fix",
"e2e": "yarn cypress install && yarn grafana-e2e run",
"e2e:update": "yarn cypress install && yarn grafana-e2e run --update-screenshots",
"server": "docker-compose up --build"
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 ./src",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 --quiet ./src",
"stylelint": "stylelint ./src/**/*.{css,scss,module.css,module.scss}",
"stylelint:fix": "stylelint --fix ./src/**/*.{css,scss,module.css,module.scss}",
"build": "grafana-toolkit plugin:build",
"build:dev": "grafana-toolkit plugin:build --skipTest --skipLint",
"test": "jest --verbose",
"dev": "grafana-toolkit plugin:dev",
"watch": "grafana-toolkit plugin:dev --watch",
"sign": "grafana-toolkit plugin:sign",
"ci-build:finish": "grafana-toolkit plugin:ci-build --finish",
"ci-package": "grafana-toolkit plugin:ci-package",
"ci-report": "grafana-toolkit plugin:ci-report",
"start": "yarn watch",
"plop": "plop",
"setversion": "setversion"
},
"lint-staged": {
"*.ts?(x)": [
"prettier --write",
"eslint --fix"
],
"*.js?(x)": [
"prettier --write",
"eslint --fix"
],
"*.css": [
"stylelint --fix"
]
},
"author": "CorpGlory Inc.",
"license": "Apache-2.0",
@ -21,6 +40,7 @@
"@grafana/e2e": "9.1.2",
"@grafana/e2e-selectors": "9.1.2",
"@grafana/eslint-config": "^2.5.0",
"@grafana/toolkit": "^9.3.2",
"@grafana/tsconfig": "^1.2.0-rc1",
"@swc/core": "^1.2.144",
"@swc/helpers": "^0.3.6",
@ -37,7 +57,10 @@
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"axios": "^1.2.1",
"circular-dependency-plugin": "^5.2.2",
"classnames": "^2.3.2",
"copy-webpack-plugin": "^10.0.0",
"css-loader": "^6.7.3",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-jsdoc": "^36.1.0",
@ -49,9 +72,13 @@
"glob": "^8.0.3",
"jest": "27.5.0",
"lodash-es": "^4.17.21",
"postcss-loader": "^7.0.2",
"prettier": "^2.5.0",
"react-copy-to-clipboard": "^5.1.0",
"replace-in-file-webpack-plugin": "^1.0.6",
"sass": "^1.57.0",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"swc-loader": "^0.1.15",
"ts-node": "^10.5.0",
"tsconfig-paths": "^3.12.0",

4
src/__mocks__/@grafana/runtime.ts

@ -0,0 +1,4 @@
export const getBackendSrv = () => ({
get: jest.fn(),
post: jest.fn(),
});

32
src/components/App/App.test.tsx

@ -1,32 +0,0 @@
import React from 'react';
import { AppRootProps, PluginType } from '@grafana/data';
import { render, screen } from '@testing-library/react';
import { App } from './App';
describe('Components/App', () => {
let props: AppRootProps;
beforeEach(() => {
jest.resetAllMocks();
props = {
basename: 'a/sample-app',
meta: {
id: 'sample-app',
name: 'Sample App',
type: PluginType.app,
enabled: true,
jsonData: {},
},
query: {},
path: '',
onNavChanged: jest.fn(),
} as unknown as AppRootProps;
});
test('renders without an error"', () => {
render(<App {...props} />);
expect(screen.queryByText(/Hello Grafana!/i)).toBeInTheDocument();
});
});

8
src/components/App/App.tsx

@ -1,8 +0,0 @@
import * as React from 'react';
import { AppRootProps } from '@grafana/data';
export class App extends React.PureComponent<AppRootProps> {
render() {
return <div className="page-container">Hello Grafana!</div>;
}
}

1
src/components/App/index.tsx

@ -1 +0,0 @@
export * from './App';

11
src/components/GBlock/Block.tsx

@ -1,7 +1,8 @@
import { css } from '@emotion/css';
import React, { FC, HTMLAttributes } from 'react';
import styles from './Block.module.scss';
import cn from 'classnames/bind';
// import './Block.module.scss';
import React, { FC, HTMLAttributes } from 'react';
interface BlockProps extends HTMLAttributes<HTMLElement> {
bordered?: boolean;
@ -11,6 +12,8 @@ interface BlockProps extends HTMLAttributes<HTMLElement> {
fullWidth?: boolean;
}
const cx = cn.bind(styles);
const Block: FC<BlockProps> = (props) => {
const {
children,
@ -26,7 +29,7 @@ const Block: FC<BlockProps> = (props) => {
return (
<div
className={css('root', className, {
className={cx('root', className, {
root_bordered: bordered,
root_shadowed: shadowed,
'root--fullWidth': fullWidth,

2
src/components/PluginConfigPage/index.tsx

@ -1 +1,3 @@
import '../../style/vars.css';
import '../../style/global.css';
export * from './PluginConfigPage';

41
src/components/PluginConfigPage/parts/ConfigurationForm/index.tsx

@ -1,18 +1,19 @@
import React, { FC, useCallback, useState } from 'react';
import { Button, Field, Form, Input } from '@grafana/ui';
// import cn from 'classnames/bind';
import { isEmpty } from 'lodash-es';
import { SubmitHandler } from 'react-hook-form';
import styles from './ConfigurationForm.module.css';
import Block from '../../../GBlock/Block';
import Text from '../../../Text/Text';
import PluginState from '../../../../plugin_state';
import { css } from '@emotion/css';
// import styles from './ConfigurationForm.module.css';
import { Button, Field, Form, Input } from '@grafana/ui';
import cn from 'classnames/bind';
import { SubmitHandler } from 'react-hook-form';
import React, { FC, useCallback, useState } from 'react';
import { isEmpty } from 'lodash-es';
// const cx = cn.bind(styles);
const cx = cn.bind(styles);
type Props = {
onSuccessfulSetup: () => void;
@ -40,13 +41,7 @@ const FormErrorMessage: FC<{ errorMsg: string }> = ({ errorMsg }) => (
<pre>
<Text type="link">{errorMsg}</Text>
</pre>
<Block
withBackground
className={css({
'margin-bottom': '24px',
'margin-top': '24px',
})}
>
<Block withBackground className={cx('info-block')}>
<Text type="secondary">
Need help?
<br />- file bugs on our GitHub Issues page{' '}
@ -86,12 +81,7 @@ const ConfigurationForm: FC<Props> = ({ onSuccessfulSetup, defaultDataExporterAp
<Form<FormProps> defaultValues={{ dataExporterApiUrl: defaultDataExporterApiUrl }} onSubmit={setupPlugin}>
{({ register, errors }) => (
<>
<div
className={css({
'margin-bottom': '24px',
'margin-top': '24px',
})}
>
<div className={cx('info-block')}>
<p>1. Launch the DataExporter backend</p>
<Text type="secondary">
Run hobby, dev or production backend. See{' '}
@ -102,12 +92,7 @@ const ConfigurationForm: FC<Props> = ({ onSuccessfulSetup, defaultDataExporterAp
</Text>
</div>
<div
className={css({
'margin-bottom': '24px',
'margin-top': '24px',
})}
>
<div className={cx('info-block')}>
<p>2. Let us know the base URL of your DataExporter API</p>
<Text type="secondary">
The DataExporter backend must be reachable from your Grafana installation. Some examples are:

36
src/components/PluginConfigPage/parts/RemoveCurrentConfigurationButton/__snapshots__/RemoveCurrentConfigurationButton.test.tsx.snap

@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RemoveCurrentConfigurationButton It renders properly when disabled 1`] = `
<body>
<div>
<button
class="css-mk7eo3-button"
disabled=""
type="button"
>
<span
class="css-1mhnkuh"
>
Remove current configuration
</span>
</button>
</div>
</body>
`;
exports[`RemoveCurrentConfigurationButton It renders properly when enabled 1`] = `
<body>
<div>
<button
class="css-mk7eo3-button"
type="button"
>
<span
class="css-1mhnkuh"
>
Remove current configuration
</span>
</button>
</div>
</body>
`;

12
src/components/PluginConfigPage/parts/StatusMessageBlock/StatusMessageBlock.test.tsx

@ -1,12 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import StatusMessageBlock from '.';
describe('StatusMessageBlock', () => {
test('It renders properly', async () => {
const component = render(<StatusMessageBlock text="helloooo" />);
expect(component.baseElement).toMatchSnapshot();
});
});

21
src/components/Text/Text.tsx

@ -1,12 +1,13 @@
import React, { FC, HTMLAttributes, ChangeEvent, useState, useCallback } from 'react';
import styles from './Text.module.scss';
import { openNotification } from '../../utils';
import { IconButton, Modal, Input, HorizontalGroup, Button, VerticalGroup } from '@grafana/ui';
import CopyToClipboard from 'react-copy-to-clipboard';
import { openNotification } from '../../utils';
import cn from 'classnames/bind';
// import './Text.module.scss';
import { css } from '@emotion/css';
import CopyToClipboard from 'react-copy-to-clipboard';
import React, { FC, HTMLAttributes, ChangeEvent, useState, useCallback } from 'react';
export type TextType = 'primary' | 'secondary' | 'disabled' | 'link' | 'success' | 'warning';
@ -32,6 +33,8 @@ interface TextInterface extends React.FC<TextProps> {
const PLACEHOLDER = '**********';
const cx = cn.bind(styles);
const Text: TextInterface = (props) => {
const {
type,
@ -78,7 +81,7 @@ const Text: TextInterface = (props) => {
return (
<span
onClick={onClick}
className={css('root', 'text', className, {
className={cx('root', 'text', className, {
[`text--${type}`]: true,
[`text--${size}`]: true,
'text--strong': strong,
@ -93,7 +96,7 @@ const Text: TextInterface = (props) => {
<IconButton
onClick={handleEditClick}
variant="primary"
className={css('icon-button')}
className={cx('icon-button')}
tooltip="Edit"
tooltipPlacement="top"
name="edit"
@ -108,7 +111,7 @@ const Text: TextInterface = (props) => {
>
<IconButton
variant="primary"
className={css('icon-button')}
className={cx('icon-button')}
tooltip="Copy to clipboard"
tooltipPlacement="top"
name="copy"
@ -153,7 +156,7 @@ const Title: FC<TitleProps> = (props) => {
const Tag: keyof JSX.IntrinsicElements = `h${level}`;
return (
<Tag className={css('title', className)} style={style}>
<Tag className={cx('title', className)} style={style}>
<Text {...restProps} />
</Tag>
);

4
src/img/logo.svg

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 81.9 71.52"><defs><style>.cls-1{fill:#84aff1;}.cls-2{fill:#3865ab;}.cls-3{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="42.95" y1="16.88" x2="81.9" y2="16.88" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M55.46,62.43A2,2,0,0,1,54.07,59l4.72-4.54a2,2,0,0,1,2.2-.39l3.65,1.63,3.68-3.64a2,2,0,1,1,2.81,2.84l-4.64,4.6a2,2,0,0,1-2.22.41L60.6,58.26l-3.76,3.61A2,2,0,0,1,55.46,62.43Z"/><path class="cls-2" d="M37,0H2A2,2,0,0,0,0,2V31.76a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V2A2,2,0,0,0,37,0ZM4,29.76V8.84H35V29.76Z"/><path class="cls-3" d="M79.9,0H45a2,2,0,0,0-2,2V31.76a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V2A2,2,0,0,0,79.9,0ZM47,29.76V8.84h31V29.76Z"/><path class="cls-2" d="M37,37.76H2a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V39.76A2,2,0,0,0,37,37.76ZM4,67.52V46.6H35V67.52Z"/><path class="cls-2" d="M79.9,37.76H45a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V39.76A2,2,0,0,0,79.9,37.76ZM47,67.52V46.6h31V67.52Z"/><rect class="cls-1" x="10.48" y="56.95" width="4" height="5.79"/><rect class="cls-1" x="17.43" y="53.95" width="4" height="8.79"/><rect class="cls-1" x="24.47" y="50.95" width="4" height="11.79"/><path class="cls-1" d="M19.47,25.8a6.93,6.93,0,1,1,6.93-6.92A6.93,6.93,0,0,1,19.47,25.8Zm0-9.85a2.93,2.93,0,1,0,2.93,2.93A2.93,2.93,0,0,0,19.47,16Z"/></g></g></svg>
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M41.1733 38.7634V42.4019C41.1733 42.7229 41.2816 42.9994 41.4982 43.2313C41.7148 43.4631 41.9856 43.5791 42.3105 43.5791C42.6354 43.5791 42.9152 43.4631 43.1498 43.2313C43.3845 42.9994 43.5018 42.7229 43.5018 42.4019V35.8204C43.5018 35.5351 43.4025 35.3032 43.204 35.1249C43.0054 34.9465 42.7798 34.8573 42.5271 34.8573H35.8123C35.4874 34.8573 35.2166 34.9732 35 35.2051C34.7834 35.437 34.6751 35.6956 34.6751 35.981C34.6751 36.302 34.7834 36.5785 35 36.8103C35.2166 37.0422 35.4874 37.1581 35.8123 37.1581H39.4946L34.4043 42.1879C34.1516 42.4376 34.0253 42.7229 34.0253 43.044C34.0253 43.365 34.1516 43.6326 34.4043 43.8466C34.6209 44.0606 34.8917 44.1677 35.2166 44.1677C35.5415 44.1677 35.8123 44.0606 36.0289 43.8466L41.1733 38.7634ZM39.0614 50C36.0289 50 33.4477 48.9477 31.3177 46.843C29.1877 44.7384 28.1227 42.2057 28.1227 39.2449C28.1227 36.2842 29.1877 33.7337 31.3177 31.5933C33.4477 29.453 36.0289 28.3829 39.0614 28.3829C42.0578 28.3829 44.63 29.453 46.778 31.5933C48.926 33.7337 50 36.2842 50 39.2449C50 42.2057 48.926 44.7384 46.778 46.843C44.63 48.9477 42.0578 50 39.0614 50ZM16.1011 17.7348H34.6751C35.2166 17.7348 35.6769 17.5476 36.056 17.173C36.435 16.7985 36.6245 16.3258 36.6245 15.7551C36.6245 15.22 36.435 14.7652 36.056 14.3906C35.6769 14.0161 35.2166 13.8288 34.6751 13.8288H16.1011C15.5235 13.8288 15.0451 14.0161 14.6661 14.3906C14.287 14.7652 14.0975 15.22 14.0975 15.7551C14.0975 16.3258 14.287 16.7985 14.6661 17.173C15.0451 17.5476 15.5235 17.7348 16.1011 17.7348ZM26.2274 45.1843H8.95307C7.83394 45.1843 6.89531 44.8097 6.13718 44.0606C5.37906 43.3115 5 42.3841 5 41.2782V8.90606C5 7.83591 5.37906 6.91736 6.13718 6.15042C6.89531 5.38347 7.83394 5 8.95307 5H41.769C42.8881 5 43.8267 5.38347 44.5848 6.15042C45.343 6.91736 45.722 7.83591 45.722 8.90606V26.9382C44.7112 26.3674 43.6372 25.9394 42.5 25.654C41.3628 25.3686 40.2166 25.2259 39.0614 25.2259C38.6643 25.2259 38.2581 25.2527 37.843 25.3062C37.4278 25.3597 37.0217 25.4221 36.6245 25.4935V24.9584C36.6245 24.459 36.417 24.0309 36.0018 23.6742C35.5866 23.3175 35.1444 23.1391 34.6751 23.1391H16.1011C15.5235 23.1391 15.0451 23.3353 14.6661 23.7277C14.287 24.1201 14.0975 24.5838 14.0975 25.1189C14.0975 25.654 14.287 26.1088 14.6661 26.4834C15.0451 26.8579 15.5235 27.0452 16.1011 27.0452H32.4007C31.1733 27.6516 30.0722 28.4185 29.0975 29.346C28.1227 30.2735 27.3285 31.3258 26.7148 32.503H16.1011C15.5235 32.503 15.0451 32.6902 14.6661 33.0648C14.287 33.4394 14.0975 33.8942 14.0975 34.4292C14.0975 35 14.287 35.4727 14.6661 35.8472C15.0451 36.2218 15.5235 36.409 16.1011 36.409H25.1986C25.0903 36.8728 25.009 37.3543 24.9549 37.8537C24.9007 38.3531 24.8736 38.8347 24.8736 39.2985C24.8736 40.3329 24.982 41.3496 25.1986 42.3484C25.4152 43.3472 25.7581 44.2925 26.2274 45.1843Z" fill="#7191ED"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

6
src/index.d.ts vendored

@ -0,0 +1,6 @@
declare module '*.css';
declare module '*.scss' {
const content: Record<string, string>;
export default content;
}

1
src/jest/grafanaMock.ts

@ -0,0 +1 @@
export default {};

15
src/jest/matchMedia.ts

@ -0,0 +1,15 @@
// @ts-ignore
export default global.matchMedia =
global.matchMedia ||
function (query) {
return {
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
};
};

1
src/jest/styleMock.ts

@ -0,0 +1 @@
export default {};

8
src/jest/svgTransform.ts

@ -0,0 +1,8 @@
module.exports = {
process() {
return { code: 'module.exports = {};' };
},
getCacheKey() {
return 'svgTransform';
},
};

13
src/jest/utils.ts

@ -0,0 +1,13 @@
export function mockUseStore() {
jest.mock('state/useStore', () => ({
useStore: () => ({
isUserActionAllowed: jest.fn().mockReturnValue(true),
}),
}));
}
export function mockGrafanaLocationSrv() {
jest.mock('@grafana/runtime', () => ({
getLocationSrv: jest.fn(),
}));
}

6
src/module.ts

@ -1,8 +1,8 @@
import { AppPlugin } from '@grafana/data';
import { App } from './components/App';
import { PluginConfigPage } from './components/PluginConfigPage';
export const plugin = new AppPlugin<{}>().setRootPage(App).addConfigPage({
import { AppPlugin } from '@grafana/data';
export const plugin = new AppPlugin<{}>().addConfigPage({
title: 'Configuration',
icon: 'fa fa-cog',
// @ts-ignore - Would expect a Class component, however works absolutely fine with a functional one

10
src/plugin.json

@ -17,16 +17,6 @@
"version": "%VERSION%",
"updated": "%TODAY%"
},
"includes": [
{
"type": "page",
"name": "Default",
"path": "/a/%PLUGIN_ID%",
"role": "Admin",
"addToNav": true,
"defaultNav": true
}
],
"routes": [
{
"path": "api/*",

45
src/style/global.css

@ -0,0 +1,45 @@
.configure-plugin {
margin-top: 10px;
}
@keyframes fadeIn {
from {
opacity: 0;
}
}
/* Spinner */
.spin {
width: 100%;
margin-top: 200px;
margin-bottom: 200px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.spin-text {
margin-top: 20px;
}
/* Tables */
.disabled-row {
background: #f0f0f0;
}
.highlighted-row {
background: var(--highlighted-row-bg);
}
/* Navigation */
.navbarRootFallback {
margin-top: 24px;
}
.page-title {
margin-bottom: 16px;
}

78
src/style/vars.css

@ -0,0 +1,78 @@
:root {
--maintenance-background: repeating-linear-gradient(45deg, #f6ba52, #f6ba52 20px, #ffd180 20px, #ffd180 40px);
--gren-5: #6ccf8e;
--green-6: #73d13d;
--red-5: #ff4d4f;
--orange-5: #ffa940;
--blue-2: #bae7ff;
--gray-5: #d9d9d9;
--gray-8: #595959;
--gray-9: #434343;
--cyan-1: #e6fffb;
--purple-9: #22075e;
--border-radius: 2px;
--gradient-brandHorizontal: linear-gradient(90deg, #f83 0%, #f53e4c 100%);
--gradient-brandVertical: linear-gradient(0.01deg, #f53e4c -31.2%, #f83 113.07%);
--always-gray: #ccccdc;
--title-marginBottom: 16px;
}
.theme-light {
--cards-background: var(--blue-2);
--highlighted-row-bg: var(--cyan-1);
--disabled-button-color: #bdbdbd;
--primary-background: rgb(255, 255, 255);
--secondary-background: rgb(244, 245, 245);
--border: 1px solid rgba(36, 41, 46, 0.12);
--primary-text-color: rgb(36, 41, 46);
--secondary-text-color: rgba(36, 41, 46, 0.75);
--disabled-text-color: rgba(36, 41, 46, 0.5);
--warning-text-color: #8a6c00;
--success-text-color: rgb(10, 118, 78);
--error-text-color: rgb(207, 14, 91);
--primary-text-link: #1f62e0;
--timeline-icon-background: rgba(70, 76, 84, 0);
--timeline-icon-background-resolution-note: rgba(50, 116, 217, 0);
--oncall-icon-stroke-color: #fff;
--hover-selected: #f4f5f5;
--background-canvas: #f4f5f5;
--background-primary: #fff;
--background-secondary: #f4f5f5;
--border-medium: 1px solid rgba(36, 41, 46, 0.3);
--border-strong: 1px solid rgba(36, 41, 46, 0.4);
--border-weak: 1px solid rgba(36, 41, 46, 0.12);
--shadows-z1: 0 1px 2px rgba(24, 26, 27, 0.2);
--shadows-z2: 0 4px 8px rgba(24, 26, 27, 0.2);
--shadows-z3: 0 13px 20px 1px rgba(24, 26, 27, 0.18);
}
.theme-dark {
--cards-background: var(--gray-9);
--highlighted-row-bg: var(--gray-9);
--disabled-button-color: hsla(0, 0%, 100%, 0.08);
--primary-background: rgb(24, 27, 31);
--secondary-background: rgb(34, 37, 43);
--border: 1px solid rgba(204, 204, 220, 0.15);
--primary-text-color: rgb(204, 204, 220);
--secondary-text-color: rgba(204, 204, 220, 0.65);
--disabled-text-color: rgba(204, 204, 220, 0.4);
--warning-text-color: #f8d06b;
--success-text-color: rgb(108, 207, 142);
--error-text-color: rgb(255, 82, 134);
--primary-text-link: #6e9fff;
--timeline-icon-background: rgba(70, 76, 84, 1);
--timeline-icon-background-resolution-note: rgba(50, 116, 217, 1);
--focused-box-shadow: rgb(17 18 23) 0 0 0 2px, rgb(61 113 217) 0 0 0 4px;
--hover-selected: rgba(204, 204, 220, 0.12);
--hover-selected-hardcoded: #34363d;
--oncall-icon-stroke-color: #181b1f;
--background-canvas: #111217;
--background-primary: #181b1f;
--background-secondary: #22252b;
--border-medium: 1px solid rgba(204, 204, 220, 0.15);
--border-strong: 1px solid rgba(204, 204, 220, 0.25);
--border-weak: 1px solid rgba(204, 204, 220, 0.07);
--shadows-z1: 0 1px 2px rgba(24, 26, 27, 0.75);
--shadows-z2: 0 4px 8px rgba(24, 26, 27, 0.75);
--shadows-z3: 0 8px 24px rgb(1, 4, 9);
}

147
webpack.config.ts

@ -0,0 +1,147 @@
const webpack = require('webpack');
const path = require('path');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const MONACO_DIR = path.resolve(__dirname, './node_modules/monaco-editor');
Object.defineProperty(RegExp.prototype, 'toJSON', {
value: RegExp.prototype.toString,
});
module.exports.getWebpackConfig = (config, options) => {
const cssLoader = config.module.rules.find((rule) => rule.test.toString() === '/\\.css$/');
cssLoader.exclude.push(/\.module\.css$/, MONACO_DIR);
const grafanaRules = config.module.rules.filter((a) => a.test.toString() !== /\.s[ac]ss$/.toString());
const newConfig = {
...config,
module: {
...config.module,
rules: [
...grafanaRules,
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false,
presets: [
[
'@babel/preset-env',
{
modules: false,
},
],
[
'@babel/preset-typescript',
{
allowNamespaces: true,
allowDeclareFields: true,
},
],
['@babel/preset-react'],
],
plugins: [
[
'@babel/plugin-transform-typescript',
{
allowNamespaces: true,
allowDeclareFields: true,
},
],
'@babel/plugin-proposal-class-properties',
[
'@babel/plugin-proposal-object-rest-spread',
{
loose: true,
},
],
[
'@babel/plugin-proposal-decorators',
{
legacy: true,
},
],
'@babel/plugin-transform-react-constant-elements',
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-syntax-dynamic-import',
],
},
},
'ts-loader',
],
},
{
test: /\.module\.css$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
sourceMap: true,
modules: {
localIdentName: options.production ? '[name]__[hash:base64]' : '[path][name]__[local]',
},
},
},
],
},
{
test: /\.module\.scss$/i,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
sourceMap: true,
modules: {
localIdentName: options.production ? '[name]__[hash:base64]' : '[path][name]__[local]',
},
},
},
'postcss-loader',
'sass-loader',
],
},
],
},
plugins: [
...config.plugins,
new CircularDependencyPlugin({
// exclude detection of files based on a RegExp
exclude: /node_modules/,
// include specific files based on a RegExp
// add errors to webpack instead of warnings
failOnError: true,
// allow import cycles that include an asyncronous import,
// e.g. via import(/* webpackMode: "weak" */ './file.js')
allowAsyncCycles: false,
// set the current working directory for displaying module paths
cwd: process.cwd(),
}),
],
resolve: {
...config.resolve,
symlinks: false,
modules: [path.resolve(__dirname, './frontend_enterprise/src'), ...config.resolve.modules],
},
};
return newConfig;
};

3560
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save