@grafana/create-plugin 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/commands/generate/print-success-message.js +1 -0
- package/dist/commands/generate.command.js +3 -0
- package/dist/constants.js +1 -0
- package/dist/utils/tests/utils.handlebars.test.js +7 -1
- package/dist/utils/tests/utils.templates.test.js +1 -1
- package/dist/utils/utils.files.js +1 -0
- package/dist/utils/utils.handlebars.js +10 -0
- package/dist/utils/utils.templates.js +6 -0
- package/package.json +2 -2
- package/src/commands/generate/print-success-message.ts +1 -0
- package/src/commands/generate.command.ts +6 -1
- package/src/constants.ts +1 -0
- package/src/types.ts +2 -0
- package/src/utils/tests/utils.handlebars.test.ts +8 -1
- package/src/utils/tests/utils.templates.test.ts +1 -1
- package/src/utils/utils.config.ts +1 -0
- package/src/utils/utils.files.ts +1 -0
- package/src/utils/utils.handlebars.ts +11 -0
- package/src/utils/utils.templates.ts +8 -2
- package/templates/app/src/components/AppConfig/AppConfig.tsx +2 -0
- package/templates/app/tests/appConfig.spec.ts +19 -0
- package/templates/app/tests/appNavigation.spec.ts +32 -0
- package/templates/app/tests/fixtures.ts +19 -0
- package/templates/backend/pkg/models/settings.go +35 -0
- package/templates/backend/pkg/plugin/datasource.go +15 -8
- package/templates/backend/tests/configEditor.spec.ts +26 -0
- package/templates/backend/tests/queryEditor.spec.ts +22 -0
- package/templates/common/_package.json +8 -6
- package/templates/common/gitignore +8 -4
- package/templates/common/npmrc +2 -2
- package/templates/common/playwright.config +53 -0
- package/templates/datasource/provisioning/datasources/datasources.yml +4 -0
- package/templates/datasource/src/datasource.ts +47 -6
- package/templates/datasource/src/types.ts +9 -0
- package/templates/datasource/tests/configEditor.spec.ts +33 -0
- package/templates/datasource/tests/queryEditor.spec.ts +11 -0
- package/templates/github/ci/.github/workflows/ci.yml +18 -4
- package/templates/panel/provisioning/dashboards/dashboard.json +35 -3
- package/templates/panel/provisioning/datasources/datasources.yml +7 -0
- package/templates/panel/src/components/SimplePanel.tsx +9 -3
- package/templates/panel/tests/panel.spec.ts +39 -0
- package/templates/common/cypress/integration/01-smoke.spec.ts +0 -10
- package/templates/common/cypress.json +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# v4.6.0 (Fri Apr 05 2024)
|
|
2
|
+
|
|
3
|
+
#### 🚀 Enhancement
|
|
4
|
+
|
|
5
|
+
- Create Plugin: Scaffold plugins with playwright for e2e [#847](https://github.com/grafana/plugin-tools/pull/847) ([@jackw](https://github.com/jackw) [@sunker](https://github.com/sunker))
|
|
6
|
+
|
|
7
|
+
#### Authors: 2
|
|
8
|
+
|
|
9
|
+
- Erik Sundell ([@sunker](https://github.com/sunker))
|
|
10
|
+
- Jack Westbrook ([@jackw](https://github.com/jackw))
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
1
14
|
# v4.5.0 (Fri Apr 05 2024)
|
|
2
15
|
|
|
3
16
|
#### 🚀 Enhancement
|
|
@@ -7,6 +7,7 @@ export function printGenerateSuccessMessage(answers) {
|
|
|
7
7
|
const commands = [
|
|
8
8
|
`- \`cd ./${directory}\``,
|
|
9
9
|
`- \`${packageManagerName} install\` to install frontend dependencies.`,
|
|
10
|
+
`- \`${packageManagerName} exec playwright install chromium\` to install e2e test dependencies.`,
|
|
10
11
|
`- \`${packageManagerName} run dev\` to build (and watch) the plugin frontend code.`,
|
|
11
12
|
...(answers.hasBackend
|
|
12
13
|
? [
|
|
@@ -48,6 +48,7 @@ function getTemplateData(answers) {
|
|
|
48
48
|
const packageManagerInstallCmd = getPackageManagerInstallCmd(packageManagerName);
|
|
49
49
|
const isAppType = pluginType === PLUGIN_TYPES.app || pluginType === PLUGIN_TYPES.scenes;
|
|
50
50
|
const useReactRouterV6 = features.useReactRouterV6 === true && pluginType === PLUGIN_TYPES.app;
|
|
51
|
+
const usePlaywright = features.usePlaywright === true;
|
|
51
52
|
const templateData = {
|
|
52
53
|
...answers,
|
|
53
54
|
pluginId,
|
|
@@ -60,6 +61,8 @@ function getTemplateData(answers) {
|
|
|
60
61
|
bundleGrafanaUI: features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI,
|
|
61
62
|
useReactRouterV6,
|
|
62
63
|
reactRouterVersion: useReactRouterV6 ? '6.22.0' : '5.2.0',
|
|
64
|
+
usePlaywright,
|
|
65
|
+
e2eTestCmd: 'playwright test',
|
|
63
66
|
};
|
|
64
67
|
return templateData;
|
|
65
68
|
}
|
package/dist/constants.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PLUGIN_TYPES } from '../../constants.js';
|
|
2
|
-
import { normalizeId } from '../utils.handlebars.js';
|
|
2
|
+
import { kebabToPascalKebab, normalizeId } from '../utils.handlebars.js';
|
|
3
3
|
describe('Handlebars helpers', () => {
|
|
4
4
|
describe('normalize id', () => {
|
|
5
5
|
test.each([PLUGIN_TYPES.app, PLUGIN_TYPES.datasource, PLUGIN_TYPES.panel])('should return the id with the type appended', (type) => {
|
|
@@ -27,4 +27,10 @@ describe('Handlebars helpers', () => {
|
|
|
27
27
|
expect(actual).toEqual(`myorg-myplugin-${type}`);
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
|
+
describe('pascal to pascal kebab', () => {
|
|
31
|
+
test('should convert pascal case to pascal kebab case', () => {
|
|
32
|
+
const actual = kebabToPascalKebab('my-plugin');
|
|
33
|
+
expect(actual).toEqual('My-Plugin');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
30
36
|
});
|
|
@@ -17,7 +17,7 @@ describe('Utils / Templates', () => {
|
|
|
17
17
|
expect(templateFiles.length).toBe(0);
|
|
18
18
|
});
|
|
19
19
|
test('should be possible to filter for multiple different files', () => {
|
|
20
|
-
const templateFiles = getTemplateFiles(PLUGIN_TYPES.app, ['.prettierrc.js', '
|
|
20
|
+
const templateFiles = getTemplateFiles(PLUGIN_TYPES.app, ['.prettierrc.js', 'jest.config.js', 'tsconfig.json']);
|
|
21
21
|
expect(Array.isArray(templateFiles)).toBe(true);
|
|
22
22
|
expect(templateFiles.length).toBe(3);
|
|
23
23
|
});
|
|
@@ -14,6 +14,15 @@ export const normalizeId = (pluginName, orgName, type) => {
|
|
|
14
14
|
const newOrgName = orgName.replace(nameRegex, '');
|
|
15
15
|
return newOrgName.toLowerCase() + '-' + newPluginName.toLowerCase() + `-${type}`;
|
|
16
16
|
};
|
|
17
|
+
export const kebabToPascalKebab = (str) => {
|
|
18
|
+
if (typeof str !== 'string') {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
return str
|
|
22
|
+
.split('-')
|
|
23
|
+
.map((word) => pascalCase(word))
|
|
24
|
+
.join('-');
|
|
25
|
+
};
|
|
17
26
|
registerHandlebarsHelpers();
|
|
18
27
|
registerHandlebarsPartials();
|
|
19
28
|
function registerHandlebarsHelpers() {
|
|
@@ -30,6 +39,7 @@ function registerHandlebarsHelpers() {
|
|
|
30
39
|
dashCase: kebabCase,
|
|
31
40
|
kabobCase: kebabCase,
|
|
32
41
|
kebabCase: kebabCase,
|
|
42
|
+
kebabToPascalKebab: kebabToPascalKebab,
|
|
33
43
|
properCase: pascalCase,
|
|
34
44
|
pascalCase: pascalCase,
|
|
35
45
|
if_eq: ifEq,
|
|
@@ -61,6 +61,10 @@ export function getTemplateData() {
|
|
|
61
61
|
const useReactRouterV6 = features.useReactRouterV6 === true && pluginJson.type === PLUGIN_TYPES.app;
|
|
62
62
|
const { packageManagerName, packageManagerVersion } = getPackageManagerWithFallback();
|
|
63
63
|
const packageManagerInstallCmd = getPackageManagerInstallCmd(packageManagerName);
|
|
64
|
+
const usePlaywright = features.usePlaywright === true || isFile(path.join(process.cwd(), 'playwright.config.ts'));
|
|
65
|
+
const e2eTestCmd = usePlaywright
|
|
66
|
+
? 'playwright test'
|
|
67
|
+
: `${packageManagerName} exec cypress install && ${packageManagerName} exec grafana-e2e run`;
|
|
64
68
|
const templateData = {
|
|
65
69
|
...EXTRA_TEMPLATE_VARIABLES,
|
|
66
70
|
pluginId: pluginJson.id,
|
|
@@ -78,6 +82,8 @@ export function getTemplateData() {
|
|
|
78
82
|
bundleGrafanaUI: features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI,
|
|
79
83
|
useReactRouterV6: useReactRouterV6,
|
|
80
84
|
reactRouterVersion: useReactRouterV6 ? '6.22.0' : '5.2.0',
|
|
85
|
+
usePlaywright,
|
|
86
|
+
e2eTestCmd,
|
|
81
87
|
};
|
|
82
88
|
debug('\nTemplate data:\n' + JSON.stringify(templateData, null, 2));
|
|
83
89
|
return templateData;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grafana/create-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"directory": "packages/create-plugin",
|
|
6
6
|
"url": "https://github.com/grafana/plugin-tools"
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"engines": {
|
|
88
88
|
"node": ">=20"
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "086e97a2aa3147c43358e269e62045cd55e97ece"
|
|
91
91
|
}
|
|
@@ -9,6 +9,7 @@ export function printGenerateSuccessMessage(answers: CliArgs) {
|
|
|
9
9
|
const commands = [
|
|
10
10
|
`- \`cd ./${directory}\``,
|
|
11
11
|
`- \`${packageManagerName} install\` to install frontend dependencies.`,
|
|
12
|
+
`- \`${packageManagerName} exec playwright install chromium\` to install e2e test dependencies.`,
|
|
12
13
|
`- \`${packageManagerName} run dev\` to build (and watch) the plugin frontend code.`,
|
|
13
14
|
...(answers.hasBackend
|
|
14
15
|
? [
|
|
@@ -59,7 +59,10 @@ function getTemplateData(answers: CliArgs) {
|
|
|
59
59
|
const { packageManagerName, packageManagerVersion } = getPackageManagerFromUserAgent();
|
|
60
60
|
const packageManagerInstallCmd = getPackageManagerInstallCmd(packageManagerName);
|
|
61
61
|
const isAppType = pluginType === PLUGIN_TYPES.app || pluginType === PLUGIN_TYPES.scenes;
|
|
62
|
-
|
|
62
|
+
// We don't enable this by default yet for new scenes plugins.
|
|
63
|
+
const useReactRouterV6 = features.useReactRouterV6 === true && pluginType === PLUGIN_TYPES.app;
|
|
64
|
+
const usePlaywright = features.usePlaywright === true;
|
|
65
|
+
|
|
63
66
|
const templateData: TemplateData = {
|
|
64
67
|
...answers,
|
|
65
68
|
pluginId,
|
|
@@ -72,6 +75,8 @@ function getTemplateData(answers: CliArgs) {
|
|
|
72
75
|
bundleGrafanaUI: features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI,
|
|
73
76
|
useReactRouterV6,
|
|
74
77
|
reactRouterVersion: useReactRouterV6 ? '6.22.0' : '5.2.0',
|
|
78
|
+
usePlaywright,
|
|
79
|
+
e2eTestCmd: 'playwright test',
|
|
75
80
|
};
|
|
76
81
|
|
|
77
82
|
return templateData;
|
package/src/constants.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PLUGIN_TYPES } from '../../constants.js';
|
|
2
|
-
import { normalizeId } from '../utils.handlebars.js';
|
|
2
|
+
import { kebabToPascalKebab, normalizeId } from '../utils.handlebars.js';
|
|
3
3
|
|
|
4
4
|
describe('Handlebars helpers', () => {
|
|
5
5
|
describe('normalize id', () => {
|
|
@@ -42,4 +42,11 @@ describe('Handlebars helpers', () => {
|
|
|
42
42
|
}
|
|
43
43
|
);
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
describe('pascal to pascal kebab', () => {
|
|
47
|
+
test('should convert pascal case to pascal kebab case', () => {
|
|
48
|
+
const actual = kebabToPascalKebab('my-plugin');
|
|
49
|
+
expect(actual).toEqual('My-Plugin');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
45
52
|
});
|
|
@@ -28,7 +28,7 @@ describe('Utils / Templates', () => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
test('should be possible to filter for multiple different files', () => {
|
|
31
|
-
const templateFiles = getTemplateFiles(PLUGIN_TYPES.app, ['.prettierrc.js', '
|
|
31
|
+
const templateFiles = getTemplateFiles(PLUGIN_TYPES.app, ['.prettierrc.js', 'jest.config.js', 'tsconfig.json']);
|
|
32
32
|
|
|
33
33
|
expect(Array.isArray(templateFiles)).toBe(true);
|
|
34
34
|
expect(templateFiles.length).toBe(3);
|
|
@@ -10,6 +10,7 @@ export type FeatureFlags = {
|
|
|
10
10
|
// If set to true, the plugin will be scaffolded with React Router v6. Defaults to true.
|
|
11
11
|
// (Attention! We always scaffold new projects with React Router v6, so if you are changing this to `false` manually you will need to make changes to the React code as well.)
|
|
12
12
|
useReactRouterV6?: boolean;
|
|
13
|
+
usePlaywright?: boolean;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
export type CreatePluginConfig = UserConfig & {
|
package/src/utils/utils.files.ts
CHANGED
|
@@ -28,6 +28,16 @@ export const normalizeId = (pluginName: string, orgName: string, type: PLUGIN_TY
|
|
|
28
28
|
return newOrgName.toLowerCase() + '-' + newPluginName.toLowerCase() + `-${type}`;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
export const kebabToPascalKebab = (str: string) => {
|
|
32
|
+
if (typeof str !== 'string') {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
return str
|
|
36
|
+
.split('-')
|
|
37
|
+
.map((word) => pascalCase(word))
|
|
38
|
+
.join('-');
|
|
39
|
+
};
|
|
40
|
+
|
|
31
41
|
// Register our helpers and partials with handlebars.
|
|
32
42
|
registerHandlebarsHelpers();
|
|
33
43
|
registerHandlebarsPartials();
|
|
@@ -46,6 +56,7 @@ function registerHandlebarsHelpers() {
|
|
|
46
56
|
dashCase: kebabCase,
|
|
47
57
|
kabobCase: kebabCase,
|
|
48
58
|
kebabCase: kebabCase,
|
|
59
|
+
kebabToPascalKebab: kebabToPascalKebab,
|
|
49
60
|
properCase: pascalCase,
|
|
50
61
|
pascalCase: pascalCase,
|
|
51
62
|
if_eq: ifEq,
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { mkdirp } from 'mkdirp';
|
|
5
5
|
import createDebug from 'debug';
|
|
6
|
-
import { filterOutCommonFiles, isFile, isFileStartingWith } from './utils.files.js';
|
|
6
|
+
import { directoryExists, filterOutCommonFiles, isFile, isFileStartingWith } from './utils.files.js';
|
|
7
7
|
import { renderHandlebarsTemplate } from './utils.handlebars.js';
|
|
8
8
|
import { getPluginJson } from './utils.plugin.js';
|
|
9
9
|
import {
|
|
@@ -17,7 +17,7 @@ import { TemplateData } from '../types.js';
|
|
|
17
17
|
import { getPackageManagerInstallCmd, getPackageManagerWithFallback } from './utils.packageManager.js';
|
|
18
18
|
import { getExportFileName } from '../utils/utils.files.js';
|
|
19
19
|
import { getVersion } from './utils.version.js';
|
|
20
|
-
import { getConfig } from './utils.config.js';
|
|
20
|
+
import { FeatureFlags, getConfig } from './utils.config.js';
|
|
21
21
|
|
|
22
22
|
const debug = createDebug('templates');
|
|
23
23
|
|
|
@@ -95,6 +95,10 @@ export function getTemplateData(): TemplateData {
|
|
|
95
95
|
const useReactRouterV6 = features.useReactRouterV6 === true && pluginJson.type === PLUGIN_TYPES.app;
|
|
96
96
|
const { packageManagerName, packageManagerVersion } = getPackageManagerWithFallback();
|
|
97
97
|
const packageManagerInstallCmd = getPackageManagerInstallCmd(packageManagerName);
|
|
98
|
+
const usePlaywright = features.usePlaywright === true || isFile(path.join(process.cwd(), 'playwright.config.ts'));
|
|
99
|
+
const e2eTestCmd = usePlaywright
|
|
100
|
+
? 'playwright test'
|
|
101
|
+
: `${packageManagerName} exec cypress install && ${packageManagerName} exec grafana-e2e run`;
|
|
98
102
|
|
|
99
103
|
const templateData = {
|
|
100
104
|
...EXTRA_TEMPLATE_VARIABLES,
|
|
@@ -113,6 +117,8 @@ export function getTemplateData(): TemplateData {
|
|
|
113
117
|
bundleGrafanaUI: features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI,
|
|
114
118
|
useReactRouterV6: useReactRouterV6,
|
|
115
119
|
reactRouterVersion: useReactRouterV6 ? '6.22.0' : '5.2.0',
|
|
120
|
+
usePlaywright,
|
|
121
|
+
e2eTestCmd,
|
|
116
122
|
};
|
|
117
123
|
|
|
118
124
|
debug('\nTemplate data:\n' + JSON.stringify(templateData, null, 2));
|
|
@@ -50,6 +50,7 @@ export const AppConfig = ({ plugin }: AppConfigProps) => {
|
|
|
50
50
|
<Field label="API Key" description="A secret key for authenticating to our custom API">
|
|
51
51
|
<SecretInput
|
|
52
52
|
width={60}
|
|
53
|
+
id="config-api-key"
|
|
53
54
|
data-testid={testIds.appConfig.apiKey}
|
|
54
55
|
name="apiKey"
|
|
55
56
|
value={state.apiKey}
|
|
@@ -64,6 +65,7 @@ export const AppConfig = ({ plugin }: AppConfigProps) => {
|
|
|
64
65
|
<Input
|
|
65
66
|
width={60}
|
|
66
67
|
name="apiUrl"
|
|
68
|
+
id="config-api-url"
|
|
67
69
|
data-testid={testIds.appConfig.apiUrl}
|
|
68
70
|
value={state.apiUrl}
|
|
69
71
|
placeholder={`E.g.: http://mywebsite.com/api/v1`}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { test, expect } from './fixtures';
|
|
2
|
+
|
|
3
|
+
test('should be possible to save app configuration', async ({ appConfigPage, page }) => {
|
|
4
|
+
const saveForm = page.getByRole('button', { name: /Save API settings/i });
|
|
5
|
+
|
|
6
|
+
// reset the configured secret
|
|
7
|
+
await page.getByRole('button', { name: /reset/i }).click();
|
|
8
|
+
|
|
9
|
+
// enter some valid values
|
|
10
|
+
await page.getByRole('textbox', { name: 'API Key' }).fill('secret-api-key');
|
|
11
|
+
await page.getByRole('textbox', { name: 'API Url' }).clear();
|
|
12
|
+
await page.getByRole('textbox', { name: 'API Url' }).fill('http://www.my-awsome-grafana-app.com/api');
|
|
13
|
+
|
|
14
|
+
// listen for the server response on the saved form
|
|
15
|
+
const saveResponse = appConfigPage.waitForSettingsResponse();
|
|
16
|
+
|
|
17
|
+
await saveForm.click();
|
|
18
|
+
await expect(saveResponse).toBeOK();
|
|
19
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pluginJson from '../src/plugin.json';
|
|
2
|
+
import { test, expect } from '@grafana/plugin-e2e';
|
|
3
|
+
import { ROUTES } from '../src/constants';
|
|
4
|
+
|
|
5
|
+
test.describe('navigating app', () => {
|
|
6
|
+
test('page one should render successfully', async ({ page }) => {
|
|
7
|
+
await page.goto(`/a/${pluginJson.id}/${ROUTES.One}`);
|
|
8
|
+
await expect(page.getByText('This is page one.')).toBeVisible();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('page two should render successfully', async ({ page }) => {
|
|
12
|
+
await page.goto(`/a/${pluginJson.id}/${ROUTES.Two}`);
|
|
13
|
+
await expect(page.getByText('This is page two.')).toBeVisible();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('page three should support an id parameter', async ({ page }) => {
|
|
17
|
+
await page.goto(`/a/${pluginJson.id}/${ROUTES.Three}/123456`);
|
|
18
|
+
await expect(page.getByText('ID: 123456')).toBeVisible();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('page three should render sucessfully', async ({ page }) => {
|
|
22
|
+
// wait for page to successfully render
|
|
23
|
+
await page.goto(`/a/${pluginJson.id}/${ROUTES.One}`);
|
|
24
|
+
await expect(page.getByText('This is page one.')).toBeVisible();
|
|
25
|
+
|
|
26
|
+
// navigating to page four with full width layout without sidebar menu
|
|
27
|
+
await page.getByText('Full-width page example').click();
|
|
28
|
+
|
|
29
|
+
// navigate back to page one
|
|
30
|
+
await page.getByRole('link', { name: 'Back', exact: true }).click();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AppConfigPage, test as base } from '@grafana/plugin-e2e';
|
|
2
|
+
import pluginJson from '../src/plugin.json';
|
|
3
|
+
|
|
4
|
+
type AppTestFixture = { appConfigPage: AppConfigPage };
|
|
5
|
+
|
|
6
|
+
export const test = base.extend<AppTestFixture>({
|
|
7
|
+
appConfigPage: async ({ page, selectors, grafanaVersion, request }, use, testInfo) => {
|
|
8
|
+
const configPage = new AppConfigPage(
|
|
9
|
+
{ page, selectors, grafanaVersion, request, testInfo },
|
|
10
|
+
{
|
|
11
|
+
pluginId: pluginJson.id,
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
await configPage.goto();
|
|
15
|
+
await use(configPage);
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export { expect } from '@grafana/plugin-e2e';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package models
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
|
|
7
|
+
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type PluginSettings struct {
|
|
11
|
+
Path string `json:"path"`
|
|
12
|
+
Secrets *SecretPluginSettings `json:"-"`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type SecretPluginSettings struct {
|
|
16
|
+
ApiKey string `json:"apiKey"`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func LoadPluginSettings(source backend.DataSourceInstanceSettings) (*PluginSettings, error) {
|
|
20
|
+
settings := PluginSettings{}
|
|
21
|
+
err := json.Unmarshal(source.JSONData, &settings)
|
|
22
|
+
if err != nil {
|
|
23
|
+
return nil, fmt.Errorf("could not unmarshal PluginSettings json: %w", err)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
settings.Secrets = loadSecretPluginSettings(source.DecryptedSecureJSONData)
|
|
27
|
+
|
|
28
|
+
return &settings, nil
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func loadSecretPluginSettings(source map[string]string) *SecretPluginSettings {
|
|
32
|
+
return &SecretPluginSettings{
|
|
33
|
+
ApiKey: source["apiKey"],
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -4,12 +4,12 @@ import (
|
|
|
4
4
|
"context"
|
|
5
5
|
"encoding/json"
|
|
6
6
|
"fmt"
|
|
7
|
-
"math/rand"
|
|
8
7
|
"time"
|
|
9
8
|
|
|
10
9
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
11
10
|
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
|
12
11
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
12
|
+
"github.com/{{ kebabCase orgName }}/{{ kebabCase pluginName }}/pkg/models"
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
// Make sure Datasource implements required interfaces. This is important to do
|
|
@@ -94,16 +94,23 @@ func (d *Datasource) query(_ context.Context, pCtx backend.PluginContext, query
|
|
|
94
94
|
// datasource configuration page which allows users to verify that
|
|
95
95
|
// a datasource is working as expected.
|
|
96
96
|
func (d *Datasource) CheckHealth(_ context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
res := &backend.CheckHealthResult{}
|
|
98
|
+
config, err := models.LoadPluginSettings(*req.PluginContext.DataSourceInstanceSettings)
|
|
99
99
|
|
|
100
|
-
if
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
if err != nil {
|
|
101
|
+
res.Status = backend.HealthStatusError
|
|
102
|
+
res.Message = "Unable to load settings"
|
|
103
|
+
return res, nil
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if config.Secrets.ApiKey == "" {
|
|
107
|
+
res.Status = backend.HealthStatusError
|
|
108
|
+
res.Message = "API key is missing"
|
|
109
|
+
return res, nil
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
return &backend.CheckHealthResult{
|
|
106
|
-
Status:
|
|
107
|
-
Message:
|
|
113
|
+
Status: backend.HealthStatusOk,
|
|
114
|
+
Message: "Data source is working",
|
|
108
115
|
}, nil
|
|
109
116
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { test, expect } from '@grafana/plugin-e2e';
|
|
2
|
+
import { MyDataSourceOptions, MySecureJsonData } from '../src/types';
|
|
3
|
+
|
|
4
|
+
test('"Save & test" should be successful when configuration is valid', async ({
|
|
5
|
+
createDataSourceConfigPage,
|
|
6
|
+
readProvisionedDataSource,
|
|
7
|
+
page,
|
|
8
|
+
}) => {
|
|
9
|
+
const ds = await readProvisionedDataSource<MyDataSourceOptions, MySecureJsonData>({ fileName: 'datasources.yml' });
|
|
10
|
+
const configPage = await createDataSourceConfigPage({ type: ds.type });
|
|
11
|
+
await page.getByRole('textbox', { name: 'Path' }).fill(ds.jsonData.path ?? '');
|
|
12
|
+
await page.getByRole('textbox', { name: 'API Key' }).fill(ds.secureJsonData?.apiKey ?? '');
|
|
13
|
+
await expect(configPage.saveAndTest()).toBeOK();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('"Save & test" should fail when configuration is invalid', async ({
|
|
17
|
+
createDataSourceConfigPage,
|
|
18
|
+
readProvisionedDataSource,
|
|
19
|
+
page,
|
|
20
|
+
}) => {
|
|
21
|
+
const ds = await readProvisionedDataSource<MyDataSourceOptions, MySecureJsonData>({ fileName: 'datasources.yml' });
|
|
22
|
+
const configPage = await createDataSourceConfigPage({ type: ds.type });
|
|
23
|
+
await page.getByRole('textbox', { name: 'Path' }).fill(ds.jsonData.path ?? '');
|
|
24
|
+
await expect(configPage.saveAndTest()).not.toBeOK();
|
|
25
|
+
await expect(configPage).toHaveAlert('error', { hasText: 'API key is missing' });
|
|
26
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { test, expect } from '@grafana/plugin-e2e';
|
|
2
|
+
|
|
3
|
+
test('should trigger new query when Constant field is changed', async ({
|
|
4
|
+
panelEditPage,
|
|
5
|
+
readProvisionedDataSource,
|
|
6
|
+
}) => {
|
|
7
|
+
const ds = await readProvisionedDataSource({ fileName: 'datasources.yml' });
|
|
8
|
+
await panelEditPage.datasource.set(ds.name);
|
|
9
|
+
await panelEditPage.getQueryEditorRow('A').getByRole('textbox', { name: 'Query Text' }).fill('test query');
|
|
10
|
+
const queryReq = panelEditPage.waitForQueryDataRequest();
|
|
11
|
+
await panelEditPage.getQueryEditorRow('A').getByRole('spinbutton').fill('10');
|
|
12
|
+
await expect(await queryReq).toBeTruthy();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('data query should return values 10 and 20', async ({ panelEditPage, readProvisionedDataSource }) => {
|
|
16
|
+
const ds = await readProvisionedDataSource({ fileName: 'datasources.yml' });
|
|
17
|
+
await panelEditPage.datasource.set(ds.name);
|
|
18
|
+
await panelEditPage.getQueryEditorRow('A').getByRole('textbox', { name: 'Query Text' }).fill('test query');
|
|
19
|
+
await panelEditPage.setVisualization('Table');
|
|
20
|
+
await expect(panelEditPage.refreshPanel()).toBeOK();
|
|
21
|
+
await expect(panelEditPage.panel.data).toContainText(['10', '20']);
|
|
22
|
+
});
|
|
@@ -10,19 +10,21 @@
|
|
|
10
10
|
"typecheck": "tsc --noEmit",
|
|
11
11
|
"lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .",
|
|
12
12
|
"lint:fix": "{{ packageManagerName }} run lint{{#if isNPM}} --{{/if}} --fix",
|
|
13
|
-
"e2e": "{{
|
|
14
|
-
"e2e:update": "{{ packageManagerName }} exec cypress install && {{ packageManagerName }} exec grafana-e2e run --update-screenshots",
|
|
13
|
+
"e2e": "{{{ e2eTestCmd }}}",{{#unless usePlaywright}}
|
|
14
|
+
"e2e:update": "{{ packageManagerName }} exec cypress install && {{ packageManagerName }} exec grafana-e2e run --update-screenshots",{{/unless}}
|
|
15
15
|
"server": "docker-compose up --build",
|
|
16
16
|
"sign": "npx --yes @grafana/sign-plugin@latest"
|
|
17
17
|
},
|
|
18
18
|
"author": "{{ sentenceCase orgName }}",
|
|
19
19
|
"license": "Apache-2.0",
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@babel/core": "^7.21.4",
|
|
21
|
+
"@babel/core": "^7.21.4",{{#unless usePlaywright}}
|
|
22
22
|
"@grafana/e2e": "{{ grafanaVersion }}",
|
|
23
|
-
"@grafana/e2e-selectors": "{{ grafanaVersion }}",
|
|
24
|
-
"@grafana/eslint-config": "^7.0.0",
|
|
25
|
-
"@grafana/
|
|
23
|
+
"@grafana/e2e-selectors": "{{ grafanaVersion }}",{{/unless}}
|
|
24
|
+
"@grafana/eslint-config": "^7.0.0",{{#if usePlaywright}}
|
|
25
|
+
"@grafana/plugin-e2e": "^0.27.0",{{/if}}
|
|
26
|
+
"@grafana/tsconfig": "^1.2.0-rc1",{{#if usePlaywright}}
|
|
27
|
+
"@playwright/test": "^1.41.2",{{/if}}
|
|
26
28
|
"@swc/core": "^1.3.90",
|
|
27
29
|
"@swc/helpers": "^0.5.0",
|
|
28
30
|
"@swc/jest": "^0.2.26",
|
|
@@ -25,11 +25,15 @@ dist/
|
|
|
25
25
|
artifacts/
|
|
26
26
|
work/
|
|
27
27
|
ci/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
|
|
29
|
+
# e2e test directories
|
|
30
|
+
/test-results/
|
|
31
|
+
/playwright-report/
|
|
32
|
+
/blob-report/
|
|
33
|
+
/playwright/.cache/
|
|
34
|
+
/playwright/.auth/
|
|
31
35
|
|
|
32
36
|
# Editor
|
|
33
37
|
.idea
|
|
34
38
|
|
|
35
|
-
.eslintcache
|
|
39
|
+
.eslintcache
|
package/templates/common/npmrc
CHANGED
|
@@ -9,6 +9,6 @@ public-hoist-pattern[]="*prettier*"
|
|
|
9
9
|
|
|
10
10
|
# Hoist all types packages to the root for better TS support
|
|
11
11
|
public-hoist-pattern[]="@types/*"
|
|
12
|
-
|
|
12
|
+
{{#unless usePlaywright}}
|
|
13
13
|
# @grafana/e2e expects cypress to exist in the root of the node_modules directory
|
|
14
|
-
public-hoist-pattern[]="*cypress*"
|
|
14
|
+
public-hoist-pattern[]="*cypress*"{{/unless}}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PluginOptions } from '@grafana/plugin-e2e';
|
|
2
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const pluginE2eAuth = `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Read environment variables from file.
|
|
9
|
+
* https://github.com/motdotla/dotenv
|
|
10
|
+
*/
|
|
11
|
+
// require('dotenv').config();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* See https://playwright.dev/docs/test-configuration.
|
|
15
|
+
*/
|
|
16
|
+
export default defineConfig<PluginOptions>({
|
|
17
|
+
testDir: './tests',
|
|
18
|
+
/* Run tests in files in parallel */
|
|
19
|
+
fullyParallel: true,
|
|
20
|
+
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
21
|
+
forbidOnly: !!process.env.CI,
|
|
22
|
+
/* Retry on CI only */
|
|
23
|
+
retries: process.env.CI ? 2 : 0,
|
|
24
|
+
/* Opt out of parallel tests on CI. */
|
|
25
|
+
workers: process.env.CI ? 1 : undefined,
|
|
26
|
+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
27
|
+
reporter: 'html',
|
|
28
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
29
|
+
use: {
|
|
30
|
+
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
31
|
+
baseURL: 'http://localhost:3000',
|
|
32
|
+
|
|
33
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
34
|
+
trace: 'on-first-retry',
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/* Configure projects for major browsers */
|
|
38
|
+
projects: [
|
|
39
|
+
// 1. Login to Grafana and store the cookie on disk for use in other tests.
|
|
40
|
+
{
|
|
41
|
+
name: 'auth',
|
|
42
|
+
testDir: pluginE2eAuth,
|
|
43
|
+
testMatch: [/.*\.js/],
|
|
44
|
+
},
|
|
45
|
+
// 2. Run tests in Google Chrome. Every test will start authenticated as admin user.
|
|
46
|
+
{
|
|
47
|
+
name: 'chromium',
|
|
48
|
+
use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json' },
|
|
49
|
+
dependencies: ['auth'],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
|
|
53
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getBackendSrv, isFetchError } from '@grafana/runtime';
|
|
1
2
|
import {
|
|
2
3
|
CoreApp,
|
|
3
4
|
DataQueryRequest,
|
|
@@ -8,11 +9,16 @@ import {
|
|
|
8
9
|
FieldType,
|
|
9
10
|
} from '@grafana/data';
|
|
10
11
|
|
|
11
|
-
import { MyQuery, MyDataSourceOptions, DEFAULT_QUERY } from './types';
|
|
12
|
+
import { MyQuery, MyDataSourceOptions, DEFAULT_QUERY, DataSourceResponse } from './types';
|
|
13
|
+
import { lastValueFrom } from 'rxjs';
|
|
14
|
+
import _ from 'lodash';
|
|
12
15
|
|
|
13
16
|
export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
|
|
14
19
|
constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
|
|
15
20
|
super(instanceSettings);
|
|
21
|
+
this.baseUrl = instanceSettings.url!;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
getDefaultQuery(_: CoreApp): Partial<MyQuery> {
|
|
@@ -43,11 +49,46 @@ export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
|
|
|
43
49
|
return { data };
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
async request(url: string, params?: string) {
|
|
53
|
+
const response = getBackendSrv().fetch<DataSourceResponse>({
|
|
54
|
+
url: `${this.baseUrl}${url}${params?.length ? `?${params}` : ''}`,
|
|
55
|
+
});
|
|
56
|
+
return lastValueFrom(response);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks whether we can connect to the API.
|
|
61
|
+
*/
|
|
46
62
|
async testDatasource() {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
const defaultErrorMessage = 'Cannot connect to API';
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await this.request('/health');
|
|
67
|
+
if (response.status === 200) {
|
|
68
|
+
return {
|
|
69
|
+
status: 'success',
|
|
70
|
+
message: 'Success',
|
|
71
|
+
};
|
|
72
|
+
} else {
|
|
73
|
+
return {
|
|
74
|
+
status: 'error',
|
|
75
|
+
message: response.statusText ? response.statusText : defaultErrorMessage,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
let message = '';
|
|
80
|
+
if (_.isString(err)) {
|
|
81
|
+
message = err;
|
|
82
|
+
} else if (isFetchError(err)) {
|
|
83
|
+
message = 'Fetch error: ' + (err.statusText ? err.statusText : defaultErrorMessage);
|
|
84
|
+
if (err.data && err.data.error && err.data.error.code) {
|
|
85
|
+
message += ': ' + err.data.error.code + '. ' + err.data.error.message;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
status: 'error',
|
|
90
|
+
message,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
52
93
|
}
|
|
53
94
|
}
|
|
@@ -10,6 +10,15 @@ export const DEFAULT_QUERY: Partial<MyQuery> = {
|
|
|
10
10
|
constant: 6.5,
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
export interface DataPoint {
|
|
14
|
+
Time: number;
|
|
15
|
+
Value: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DataSourceResponse {
|
|
19
|
+
datapoints: DataPoint[];
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* These are options configured for each DataSource instance
|
|
15
24
|
*/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { test, expect } from '@grafana/plugin-e2e';
|
|
2
|
+
import { MyDataSourceOptions, MySecureJsonData } from '../src/types';
|
|
3
|
+
|
|
4
|
+
test('"Save & test" should be successful when configuration is valid', async ({
|
|
5
|
+
createDataSourceConfigPage,
|
|
6
|
+
readProvisionedDataSource,
|
|
7
|
+
selectors,
|
|
8
|
+
page,
|
|
9
|
+
}) => {
|
|
10
|
+
const ds = await readProvisionedDataSource({ fileName: 'datasources.yml' });
|
|
11
|
+
const configPage = await createDataSourceConfigPage({ type: ds.type });
|
|
12
|
+
const healthCheckPath = `${selectors.apis.DataSource.proxy(
|
|
13
|
+
configPage.datasource.uid,
|
|
14
|
+
configPage.datasource.id.toString()
|
|
15
|
+
)}/health`;
|
|
16
|
+
await page.route(healthCheckPath, async (route) => await route.fulfill({ status: 200, body: 'OK' }));
|
|
17
|
+
await expect(configPage.saveAndTest({ path: healthCheckPath })).toBeOK();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('"Save & test" should display success alert box when config is valid', async ({
|
|
21
|
+
createDataSourceConfigPage,
|
|
22
|
+
readProvisionedDataSource,
|
|
23
|
+
selectors,
|
|
24
|
+
}) => {
|
|
25
|
+
const ds = await readProvisionedDataSource({ fileName: 'datasources.yml' });
|
|
26
|
+
const configPage = await createDataSourceConfigPage({ type: ds.type });
|
|
27
|
+
const healthCheckPath = `${selectors.apis.DataSource.proxy(
|
|
28
|
+
configPage.datasource.uid,
|
|
29
|
+
configPage.datasource.id.toString()
|
|
30
|
+
)}/health`;
|
|
31
|
+
await expect(configPage.saveAndTest({ path: healthCheckPath })).not.toBeOK();
|
|
32
|
+
await expect(configPage).toHaveAlert('error');
|
|
33
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { test, expect } from '@grafana/plugin-e2e';
|
|
2
|
+
|
|
3
|
+
test('data query should return a value', async ({ panelEditPage, readProvisionedDataSource }) => {
|
|
4
|
+
const ds = await readProvisionedDataSource({ fileName: 'datasources.yml' });
|
|
5
|
+
await panelEditPage.datasource.set(ds.name);
|
|
6
|
+
await panelEditPage.setVisualization('Table');
|
|
7
|
+
await panelEditPage.getQueryEditorRow('A').getByRole('textbox', { name: 'Query Text' }).fill('test query');
|
|
8
|
+
await panelEditPage.getQueryEditorRow('A').getByRole('spinbutton').fill('10');
|
|
9
|
+
await expect(panelEditPage.panel.fieldNames).toContainText(['Time', 'Value']);
|
|
10
|
+
await expect(panelEditPage.panel.data).toContainText(['10']);
|
|
11
|
+
});
|
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
{{#if_eq packageManagerName "pnpm"}}
|
|
24
24
|
# pnpm action uses the packageManager field in package.json to
|
|
25
25
|
# understand which version to install.
|
|
26
|
-
- uses: pnpm/action-setup@
|
|
26
|
+
- uses: pnpm/action-setup@v3
|
|
27
27
|
{{/if_eq}}
|
|
28
28
|
- name: Setup Node.js environment
|
|
29
29
|
uses: actions/setup-node@v4
|
|
@@ -74,16 +74,30 @@ jobs:
|
|
|
74
74
|
- name: Check for E2E
|
|
75
75
|
id: check-for-e2e
|
|
76
76
|
run: |
|
|
77
|
-
if [ -
|
|
77
|
+
if [ -f "playwright.config.ts" ]
|
|
78
78
|
then
|
|
79
79
|
echo "has-e2e=true" >> $GITHUB_OUTPUT
|
|
80
80
|
fi
|
|
81
81
|
|
|
82
|
+
- name: Install Playwright Browsers
|
|
83
|
+
if: steps.check-for-e2e.outputs.has-e2e == 'true'
|
|
84
|
+
run: {{ packageManagerName }} exec playwright install --with-deps
|
|
85
|
+
|
|
82
86
|
- name: Start grafana docker
|
|
83
87
|
if: steps.check-for-e2e.outputs.has-e2e == 'true'
|
|
84
88
|
run: docker-compose up -d
|
|
85
89
|
|
|
90
|
+
- name: Wait for Grafana to start
|
|
91
|
+
if: steps.check-for-e2e.outputs.has-e2e == 'true'
|
|
92
|
+
uses: nev7n/wait_for_response@v1
|
|
93
|
+
with:
|
|
94
|
+
url: 'http://localhost:3000/'
|
|
95
|
+
responseCode: 200
|
|
96
|
+
timeout: 60000
|
|
97
|
+
interval: 500
|
|
98
|
+
|
|
86
99
|
- name: Run e2e tests
|
|
100
|
+
id: run-e2e-tests
|
|
87
101
|
if: steps.check-for-e2e.outputs.has-e2e == 'true'
|
|
88
102
|
run: {{ packageManagerName }} run e2e
|
|
89
103
|
|
|
@@ -95,8 +109,8 @@ jobs:
|
|
|
95
109
|
uses: actions/upload-artifact@v4
|
|
96
110
|
if: steps.check-for-e2e.outputs.has-e2e == 'true' && steps.run-e2e-tests.outcome != 'success'
|
|
97
111
|
with:
|
|
98
|
-
name:
|
|
99
|
-
path:
|
|
112
|
+
name: playwright-report
|
|
113
|
+
path: playwright-report/
|
|
100
114
|
retention-days: 5
|
|
101
115
|
|
|
102
116
|
- name: Sign plugin
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"editable": true,
|
|
19
19
|
"fiscalYearStartMonth": 0,
|
|
20
20
|
"graphTooltip": 0,
|
|
21
|
+
"id": 1,
|
|
21
22
|
"links": [],
|
|
22
23
|
"liveNow": false,
|
|
23
24
|
"panels": [
|
|
@@ -50,11 +51,41 @@
|
|
|
50
51
|
],
|
|
51
52
|
"title": "Panel Title",
|
|
52
53
|
"type": "{{ pluginId }}"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"datasource": {
|
|
57
|
+
"type": "grafana-testdata-datasource",
|
|
58
|
+
"uid": "trlxrdZVk"
|
|
59
|
+
},
|
|
60
|
+
"gridPos": {
|
|
61
|
+
"h": 8,
|
|
62
|
+
"w": 12,
|
|
63
|
+
"x": 12,
|
|
64
|
+
"y": 0
|
|
65
|
+
},
|
|
66
|
+
"id": 2,
|
|
67
|
+
"options": {
|
|
68
|
+
"seriesCountSize": "sm",
|
|
69
|
+
"showSeriesCount": false,
|
|
70
|
+
"text": "Default value of text input option"
|
|
71
|
+
},
|
|
72
|
+
"targets": [
|
|
73
|
+
{
|
|
74
|
+
"alias": "",
|
|
75
|
+
"datasource": {
|
|
76
|
+
"type": "grafana-testdata-datasource",
|
|
77
|
+
"uid": "db84e60d-b92a-4089-82cb-34842fb1754b"
|
|
78
|
+
},
|
|
79
|
+
"refId": "A",
|
|
80
|
+
"scenarioId": "no_data_points"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"title": "Panel Title",
|
|
84
|
+
"type": "{{ pluginId }}"
|
|
53
85
|
}
|
|
54
86
|
],
|
|
55
87
|
"refresh": "",
|
|
56
|
-
"schemaVersion":
|
|
57
|
-
"style": "dark",
|
|
88
|
+
"schemaVersion": 39,
|
|
58
89
|
"tags": [],
|
|
59
90
|
"templating": {
|
|
60
91
|
"list": []
|
|
@@ -66,6 +97,7 @@
|
|
|
66
97
|
"timepicker": {},
|
|
67
98
|
"timezone": "",
|
|
68
99
|
"title": "Provisioned {{ pluginName }} dashboard",
|
|
69
|
-
"
|
|
100
|
+
"uid": "a538aeff-5a8a-42a5-901c-938d896fdd6f",
|
|
101
|
+
"version": 1,
|
|
70
102
|
"weekStart": ""
|
|
71
103
|
}
|
|
@@ -3,6 +3,7 @@ import { PanelProps } from '@grafana/data';
|
|
|
3
3
|
import { SimpleOptions } from 'types';
|
|
4
4
|
import { css, cx } from '@emotion/css';
|
|
5
5
|
import { useStyles2, useTheme2 } from '@grafana/ui';
|
|
6
|
+
import { PanelDataErrorView } from '@grafana/runtime';
|
|
6
7
|
|
|
7
8
|
interface Props extends PanelProps<SimpleOptions> {}
|
|
8
9
|
|
|
@@ -26,9 +27,14 @@ const getStyles = () => {
|
|
|
26
27
|
};
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
export const SimplePanel: React.FC<Props> = ({ options, data, width, height }) => {
|
|
30
|
+
export const SimplePanel: React.FC<Props> = ({ options, data, width, height, fieldConfig, id }) => {
|
|
30
31
|
const theme = useTheme2();
|
|
31
32
|
const styles = useStyles2(getStyles);
|
|
33
|
+
|
|
34
|
+
if (data.series.length === 0) {
|
|
35
|
+
return <PanelDataErrorView fieldConfig={fieldConfig} panelId={id} data={data} needsStringField />;
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
return (
|
|
33
39
|
<div
|
|
34
40
|
className={cx(
|
|
@@ -49,12 +55,12 @@ export const SimplePanel: React.FC<Props> = ({ options, data, width, height }) =
|
|
|
49
55
|
>
|
|
50
56
|
<g>
|
|
51
57
|
{{!-- /* 🚨 Escaping the following line because of Handlebars. (this comment is going to be removed after scaffolding) 🚨 */ --}}
|
|
52
|
-
<circle style=\{{ fill: theme.colors.primary.main }} r={100} />
|
|
58
|
+
<circle data-testid="simple-panel-circle" style=\{{ fill: theme.colors.primary.main }} r={100} />
|
|
53
59
|
</g>
|
|
54
60
|
</svg>
|
|
55
61
|
|
|
56
62
|
<div className={styles.textBox}>
|
|
57
|
-
{options.showSeriesCount && <div>Number of series: {data.series.length}</div>}
|
|
63
|
+
{options.showSeriesCount && <div data-testid="simple-panel-series-counter">Number of series: {data.series.length}</div>}
|
|
58
64
|
<div>Text option value: {options.text}</div>
|
|
59
65
|
</div>
|
|
60
66
|
</div>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { test, expect } from '@grafana/plugin-e2e';
|
|
2
|
+
|
|
3
|
+
test('should display "No data" in case panel data is empty', async ({
|
|
4
|
+
gotoPanelEditPage,
|
|
5
|
+
readProvisionedDashboard,
|
|
6
|
+
}) => {
|
|
7
|
+
const dashboard = await readProvisionedDashboard({ fileName: 'dashboard.json' });
|
|
8
|
+
const panelEditPage = await gotoPanelEditPage({ dashboard, id: '2' });
|
|
9
|
+
await expect(panelEditPage.panel.locator).toContainText('No data');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('should display circle when data is passed to the panel', async ({
|
|
13
|
+
panelEditPage,
|
|
14
|
+
readProvisionedDataSource,
|
|
15
|
+
page,
|
|
16
|
+
}) => {
|
|
17
|
+
const ds = await readProvisionedDataSource({ fileName: 'datasources.yml' });
|
|
18
|
+
await panelEditPage.datasource.set(ds.name);
|
|
19
|
+
await panelEditPage.setVisualization('{{kebabToPascalKebab pluginName }}');
|
|
20
|
+
await expect(page.getByTestId('simple-panel-circle')).toBeVisible();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should display series counter when "Show series counter" option is enabled', async ({
|
|
24
|
+
panelEditPage,
|
|
25
|
+
readProvisionedDataSource,
|
|
26
|
+
page,
|
|
27
|
+
selectors,
|
|
28
|
+
}) => {
|
|
29
|
+
const ds = await readProvisionedDataSource({ fileName: 'datasources.yml' });
|
|
30
|
+
await panelEditPage.datasource.set(ds.name);
|
|
31
|
+
await panelEditPage.setVisualization('{{kebabToPascalKebab pluginName }}');
|
|
32
|
+
await panelEditPage.collapseSection('{{kebabToPascalKebab pluginName }}');
|
|
33
|
+
await expect(page.getByTestId('simple-panel-circle')).toBeVisible();
|
|
34
|
+
const showSeriesSwitch = panelEditPage
|
|
35
|
+
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('{{kebabToPascalKebab pluginName }} Show series counter'))
|
|
36
|
+
.getByLabel('Toggle switch');
|
|
37
|
+
await showSeriesSwitch.click();
|
|
38
|
+
await expect(page.getByTestId('simple-panel-series-counter')).toBeVisible();
|
|
39
|
+
});
|