@grafana/create-plugin 1.4.0 → 1.4.1-canary.250.42054cd.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/dist/commands/generate.plopfile.js +7 -6
- package/dist/constants.js +2 -0
- package/package.json +4 -2
- package/scripts/generate-scenes-app.js +24 -0
- package/src/commands/generate.plopfile.ts +10 -8
- package/src/commands/types.ts +8 -0
- package/src/constants.ts +2 -0
- package/templates/common/package.json +6 -5
- package/templates/scenes-app/README.md +23 -0
- package/templates/scenes-app/provisioning/datasources/default.yaml +6 -0
- package/templates/scenes-app/src/components/App/App.tsx +14 -0
- package/templates/scenes-app/src/components/App/index.tsx +1 -0
- package/templates/scenes-app/src/components/AppConfig/AppConfig.tsx +193 -0
- package/templates/scenes-app/src/components/AppConfig/index.tsx +1 -0
- package/templates/scenes-app/src/components/Routes/Routes.tsx +18 -0
- package/templates/scenes-app/src/components/Routes/index.tsx +1 -0
- package/templates/scenes-app/src/components/SecretInput/SecretInput.tsx +25 -0
- package/templates/scenes-app/src/components/SecretInput/index.tsx +1 -0
- package/templates/scenes-app/src/components/testIds.ts +22 -0
- package/templates/scenes-app/src/constants.ts +14 -0
- package/templates/scenes-app/src/img/logo.svg +1 -0
- package/templates/scenes-app/src/module.ts +10 -0
- package/templates/scenes-app/src/pages/Home/CustomSceneObject.tsx +31 -0
- package/templates/scenes-app/src/pages/Home/Home.tsx +49 -0
- package/templates/scenes-app/src/pages/Home/index.tsx +1 -0
- package/templates/scenes-app/src/pages/Home/scenes.tsx +100 -0
- package/templates/scenes-app/src/pages/WithDrilldown/WithDrilldown.tsx +97 -0
- package/templates/scenes-app/src/pages/WithDrilldown/index.tsx +1 -0
- package/templates/scenes-app/src/pages/WithDrilldown/panels.tsx +188 -0
- package/templates/scenes-app/src/pages/WithDrilldown/scenes.tsx +122 -0
- package/templates/scenes-app/src/pages/WithTabs/WithTabs.tsx +45 -0
- package/templates/scenes-app/src/pages/WithTabs/index.tsx +1 -0
- package/templates/scenes-app/src/plugin.json +54 -0
- package/templates/scenes-app/src/utils/utils.plugin.ts +17 -0
- package/templates/scenes-app/src/utils/utils.routing.ts +6 -0
|
@@ -71,7 +71,7 @@ function default_1(plop) {
|
|
|
71
71
|
{
|
|
72
72
|
name: 'pluginType',
|
|
73
73
|
type: 'list',
|
|
74
|
-
choices: [constants_1.PLUGIN_TYPES.app, constants_1.PLUGIN_TYPES.datasource, constants_1.PLUGIN_TYPES.panel],
|
|
74
|
+
choices: [constants_1.PLUGIN_TYPES.app, constants_1.PLUGIN_TYPES.datasource, constants_1.PLUGIN_TYPES.panel, constants_1.PLUGIN_TYPES.scenes],
|
|
75
75
|
message: 'What type of plugin would you like?'
|
|
76
76
|
},
|
|
77
77
|
{
|
|
@@ -104,7 +104,8 @@ function default_1(plop) {
|
|
|
104
104
|
pluginId: pluginId,
|
|
105
105
|
packageManagerName: packageManagerName,
|
|
106
106
|
packageManagerInstallCmd: packageManagerInstallCmd,
|
|
107
|
-
packageManagerVersion: packageManagerVersion
|
|
107
|
+
packageManagerVersion: packageManagerVersion,
|
|
108
|
+
isAppType: pluginType === constants_1.PLUGIN_TYPES.app || pluginType === constants_1.PLUGIN_TYPES.scenes
|
|
108
109
|
};
|
|
109
110
|
var commonActions = getActionsForTemplateFolder({
|
|
110
111
|
folderPath: constants_1.TEMPLATE_PATHS.common,
|
|
@@ -113,11 +114,12 @@ function default_1(plop) {
|
|
|
113
114
|
});
|
|
114
115
|
var pluginTypeSpecificActions = getActionsForTemplateFolder({
|
|
115
116
|
folderPath: constants_1.TEMPLATE_PATHS[pluginType],
|
|
116
|
-
exportPath: exportPath
|
|
117
|
+
exportPath: exportPath,
|
|
118
|
+
templateData: templateData
|
|
117
119
|
});
|
|
118
120
|
var backendFolderPath = pluginType === constants_1.PLUGIN_TYPES.app ? constants_1.TEMPLATE_PATHS.backendApp : constants_1.TEMPLATE_PATHS.backend;
|
|
119
121
|
var backendActions = hasBackend
|
|
120
|
-
? getActionsForTemplateFolder({ folderPath: backendFolderPath, exportPath: exportPath })
|
|
122
|
+
? getActionsForTemplateFolder({ folderPath: backendFolderPath, exportPath: exportPath, templateData: templateData })
|
|
121
123
|
: [];
|
|
122
124
|
var pluginActions = __spreadArray(__spreadArray(__spreadArray([], backendActions, true), pluginTypeSpecificActions, true), commonActions, true).reduce(function (acc, file) {
|
|
123
125
|
var actionExists = acc.find(function (f) { return f.path === file.path; });
|
|
@@ -163,7 +165,6 @@ function getActionsForReadme(_a) {
|
|
|
163
165
|
];
|
|
164
166
|
}
|
|
165
167
|
function replacePatternWithTemplateInReadme(pattern, partialsFile, exportPath, templateData) {
|
|
166
|
-
if (templateData === void 0) { templateData = {}; }
|
|
167
168
|
return {
|
|
168
169
|
type: 'modify',
|
|
169
170
|
path: path_1["default"].join(exportPath, 'README.md'),
|
|
@@ -174,7 +175,7 @@ function replacePatternWithTemplateInReadme(pattern, partialsFile, exportPath, t
|
|
|
174
175
|
};
|
|
175
176
|
}
|
|
176
177
|
function getActionsForTemplateFolder(_a) {
|
|
177
|
-
var folderPath = _a.folderPath, exportPath = _a.exportPath,
|
|
178
|
+
var folderPath = _a.folderPath, exportPath = _a.exportPath, templateData = _a.templateData;
|
|
178
179
|
var files = glob_1["default"].sync("".concat(folderPath, "/**"), { dot: true });
|
|
179
180
|
if (templateData.packageManagerName !== 'pnpm') {
|
|
180
181
|
files = files.filter(function (file) { return path_1["default"].basename(file) !== 'npmrc'; });
|
package/dist/constants.js
CHANGED
|
@@ -14,6 +14,7 @@ exports.PARTIALS_DIR = path_1["default"].join(exports.TEMPLATES_DIR, '_partials'
|
|
|
14
14
|
exports.FIXTURES_PATH = path_1["default"].join(__dirname, '..', 'fixtures');
|
|
15
15
|
exports.TEMPLATE_PATHS = {
|
|
16
16
|
app: path_1["default"].join(exports.TEMPLATES_DIR, 'app'),
|
|
17
|
+
scenesapp: path_1["default"].join(exports.TEMPLATES_DIR, 'scenes-app'),
|
|
17
18
|
backend: path_1["default"].join(exports.TEMPLATES_DIR, 'backend'),
|
|
18
19
|
backendApp: path_1["default"].join(exports.TEMPLATES_DIR, 'backend-app'),
|
|
19
20
|
common: path_1["default"].join(exports.TEMPLATES_DIR, 'common'),
|
|
@@ -28,6 +29,7 @@ var PLUGIN_TYPES;
|
|
|
28
29
|
PLUGIN_TYPES["panel"] = "panel";
|
|
29
30
|
PLUGIN_TYPES["datasource"] = "datasource";
|
|
30
31
|
PLUGIN_TYPES["secretsmanager"] = "secretsmanager";
|
|
32
|
+
PLUGIN_TYPES["scenes"] = "scenesapp";
|
|
31
33
|
})(PLUGIN_TYPES = exports.PLUGIN_TYPES || (exports.PLUGIN_TYPES = {}));
|
|
32
34
|
exports.EXTRA_TEMPLATE_VARIABLES = {
|
|
33
35
|
grafanaVersion: '9.5.2'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grafana/create-plugin",
|
|
3
|
-
"version": "1.4.0",
|
|
3
|
+
"version": "1.4.1-canary.250.42054cd.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"repository": {
|
|
6
6
|
"directory": "packages/create-plugin",
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
"build": "yarn clean && tsc && chmod +x ./dist/bin/run.js",
|
|
20
20
|
"dev": "CREATE_PLUGIN_DEV=true nodemon --exec 'tsc'",
|
|
21
21
|
"dev-app": "CREATE_PLUGIN_DEV=true nodemon --exec 'yarn generate-app'",
|
|
22
|
+
"dev-scenes-app": "CREATE_PLUGIN_DEV=true nodemon --exec 'yarn generate-scenes-app'",
|
|
22
23
|
"dev-panel": "CREATE_PLUGIN_DEV=true nodemon --exec 'yarn generate-panel'",
|
|
23
24
|
"dev-datasource": "CREATE_PLUGIN_DEV=true nodemon --exec 'yarn generate-datasource'",
|
|
24
25
|
"generate-app": "tsc && yarn clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-app.js",
|
|
26
|
+
"generate-scenes-app": "tsc && yarn clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-scenes-app.js",
|
|
25
27
|
"generate-app-backend": "tsc && yarn clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-app-backend.js",
|
|
26
28
|
"generate-panel": "tsc && yarn clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-panel.js",
|
|
27
29
|
"generate-datasource": "tsc && yarn clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-datasource-without-backend.js",
|
|
@@ -64,5 +66,5 @@
|
|
|
64
66
|
"engines": {
|
|
65
67
|
"node": ">=16"
|
|
66
68
|
},
|
|
67
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "42054cd68dfff83c9c2b99dcfc1df055a801ec2a"
|
|
68
70
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const nodePlop = require('node-plop');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
async function generate() {
|
|
6
|
+
const plopFile = path.join(__dirname, '../dist/commands/generate.plopfile.js');
|
|
7
|
+
if (!fs.existsSync(plopFile)) {
|
|
8
|
+
console.error('Run build first');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const plop = await nodePlop(plopFile);
|
|
12
|
+
const generator = plop.getGenerator('create-plugin');
|
|
13
|
+
await generator.runActions({
|
|
14
|
+
pluginName: 'my-plugin',
|
|
15
|
+
orgName: 'my-org',
|
|
16
|
+
pluginDescription: 'Auto-generated scenes app',
|
|
17
|
+
pluginType: 'scenesapp',
|
|
18
|
+
hasBackend: false,
|
|
19
|
+
hasGithubWorkflows: true,
|
|
20
|
+
hasGithubLevitateWorkflow: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
generate();
|
|
@@ -8,7 +8,7 @@ import { getExportPath } from '../utils/utils.path';
|
|
|
8
8
|
import { getPackageManagerInstallCmd, getPackageManagerFromUserAgent } from '../utils/utils.packageManager';
|
|
9
9
|
import { printGenerateSuccessMessage } from './generate-actions/print-success-message';
|
|
10
10
|
import { updateGoSdkAndModules } from './generate-actions/update-go-sdk-and-packages';
|
|
11
|
-
import { CliArgs } from './types';
|
|
11
|
+
import { CliArgs, TemplateData } from './types';
|
|
12
12
|
|
|
13
13
|
// Plopfile API documentation: https://plopjs.com/documentation/#plopfile-api
|
|
14
14
|
export default function (plop: NodePlopAPI) {
|
|
@@ -52,7 +52,7 @@ export default function (plop: NodePlopAPI) {
|
|
|
52
52
|
{
|
|
53
53
|
name: 'pluginType',
|
|
54
54
|
type: 'list',
|
|
55
|
-
choices: [PLUGIN_TYPES.app, PLUGIN_TYPES.datasource, PLUGIN_TYPES.panel],
|
|
55
|
+
choices: [PLUGIN_TYPES.app, PLUGIN_TYPES.datasource, PLUGIN_TYPES.panel, PLUGIN_TYPES.scenes],
|
|
56
56
|
message: 'What type of plugin would you like?',
|
|
57
57
|
},
|
|
58
58
|
{
|
|
@@ -88,11 +88,12 @@ export default function (plop: NodePlopAPI) {
|
|
|
88
88
|
// Support the users package manager of choice.
|
|
89
89
|
const { packageManagerName, packageManagerVersion } = getPackageManagerFromUserAgent();
|
|
90
90
|
const packageManagerInstallCmd = getPackageManagerInstallCmd(packageManagerName);
|
|
91
|
-
const templateData = {
|
|
91
|
+
const templateData: TemplateData = {
|
|
92
92
|
pluginId,
|
|
93
93
|
packageManagerName,
|
|
94
94
|
packageManagerInstallCmd,
|
|
95
95
|
packageManagerVersion,
|
|
96
|
+
isAppType: pluginType === PLUGIN_TYPES.app || pluginType === PLUGIN_TYPES.scenes,
|
|
96
97
|
};
|
|
97
98
|
// Copy over files that are shared between plugins types
|
|
98
99
|
const commonActions = getActionsForTemplateFolder({
|
|
@@ -105,12 +106,13 @@ export default function (plop: NodePlopAPI) {
|
|
|
105
106
|
const pluginTypeSpecificActions = getActionsForTemplateFolder({
|
|
106
107
|
folderPath: TEMPLATE_PATHS[pluginType],
|
|
107
108
|
exportPath,
|
|
109
|
+
templateData,
|
|
108
110
|
});
|
|
109
111
|
|
|
110
112
|
// Copy over backend-specific files (if selected)
|
|
111
113
|
const backendFolderPath = pluginType === PLUGIN_TYPES.app ? TEMPLATE_PATHS.backendApp : TEMPLATE_PATHS.backend;
|
|
112
114
|
const backendActions = hasBackend
|
|
113
|
-
? getActionsForTemplateFolder({ folderPath: backendFolderPath, exportPath })
|
|
115
|
+
? getActionsForTemplateFolder({ folderPath: backendFolderPath, exportPath, templateData })
|
|
114
116
|
: [];
|
|
115
117
|
|
|
116
118
|
// Common, pluginType and backend actions can contain different templates for the same destination.
|
|
@@ -168,7 +170,7 @@ function getActionsForReadme({
|
|
|
168
170
|
templateData,
|
|
169
171
|
}: {
|
|
170
172
|
exportPath: string;
|
|
171
|
-
templateData:
|
|
173
|
+
templateData: TemplateData;
|
|
172
174
|
}): ModifyActionConfig[] {
|
|
173
175
|
return [
|
|
174
176
|
replacePatternWithTemplateInReadme(
|
|
@@ -196,7 +198,7 @@ function replacePatternWithTemplateInReadme(
|
|
|
196
198
|
pattern: string,
|
|
197
199
|
partialsFile: string,
|
|
198
200
|
exportPath: string,
|
|
199
|
-
templateData:
|
|
201
|
+
templateData: TemplateData
|
|
200
202
|
): ModifyActionConfig {
|
|
201
203
|
return {
|
|
202
204
|
type: 'modify',
|
|
@@ -216,11 +218,11 @@ function replacePatternWithTemplateInReadme(
|
|
|
216
218
|
function getActionsForTemplateFolder({
|
|
217
219
|
folderPath,
|
|
218
220
|
exportPath,
|
|
219
|
-
templateData
|
|
221
|
+
templateData,
|
|
220
222
|
}: {
|
|
221
223
|
folderPath: string;
|
|
222
224
|
exportPath: string;
|
|
223
|
-
templateData
|
|
225
|
+
templateData: TemplateData;
|
|
224
226
|
}) {
|
|
225
227
|
let files = glob.sync(`${folderPath}/**`, { dot: true });
|
|
226
228
|
|
package/src/commands/types.ts
CHANGED
|
@@ -9,3 +9,11 @@ export type CliArgs = {
|
|
|
9
9
|
hasGithubWorkflows: boolean;
|
|
10
10
|
hasGithubLevitateWorkflow: boolean;
|
|
11
11
|
};
|
|
12
|
+
|
|
13
|
+
export type TemplateData = {
|
|
14
|
+
pluginId: string;
|
|
15
|
+
packageManagerName: string;
|
|
16
|
+
packageManagerInstallCmd: string;
|
|
17
|
+
packageManagerVersion: string;
|
|
18
|
+
isAppType: boolean;
|
|
19
|
+
};
|
package/src/constants.ts
CHANGED
|
@@ -18,6 +18,7 @@ export const FIXTURES_PATH = path.join(__dirname, '..', 'fixtures');
|
|
|
18
18
|
|
|
19
19
|
export const TEMPLATE_PATHS: Record<string, string> = {
|
|
20
20
|
app: path.join(TEMPLATES_DIR, 'app'),
|
|
21
|
+
scenesapp: path.join(TEMPLATES_DIR, 'scenes-app'),
|
|
21
22
|
backend: path.join(TEMPLATES_DIR, 'backend'),
|
|
22
23
|
backendApp: path.join(TEMPLATES_DIR, 'backend-app'),
|
|
23
24
|
common: path.join(TEMPLATES_DIR, 'common'),
|
|
@@ -32,6 +33,7 @@ export enum PLUGIN_TYPES {
|
|
|
32
33
|
panel = 'panel',
|
|
33
34
|
datasource = 'datasource',
|
|
34
35
|
secretsmanager = 'secretsmanager',
|
|
36
|
+
scenes = 'scenesapp',
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
// This gets merged into variables coming from user prompts (when using Plop) or any other dynamic variables,
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"@testing-library/react": "^12.1.4",
|
|
31
31
|
"@types/jest": "^29.5.0",
|
|
32
32
|
"@types/lodash": "^4.14.194",
|
|
33
|
-
"@types/node": "^18.15.11",{{#
|
|
34
|
-
"@types/react-router-dom": "^5.3.3",{{/
|
|
33
|
+
"@types/node": "^18.15.11",{{#if isAppType}}
|
|
34
|
+
"@types/react-router-dom": "^5.3.3",{{/if}}
|
|
35
35
|
"copy-webpack-plugin": "^11.0.0",
|
|
36
36
|
"css-loader": "^6.7.3",
|
|
37
37
|
"eslint-webpack-plugin": "^4.0.1",
|
|
@@ -60,11 +60,12 @@
|
|
|
60
60
|
"@emotion/css": "^11.1.3",
|
|
61
61
|
"@grafana/data": "{{ grafanaVersion }}",
|
|
62
62
|
"@grafana/runtime": "{{ grafanaVersion }}",
|
|
63
|
-
"@grafana/ui": "{{ grafanaVersion }}",
|
|
63
|
+
"@grafana/ui": "{{ grafanaVersion }}",{{#if_eq pluginType "scenesapp"}}
|
|
64
|
+
"@grafana/scenes": "^0.7.0",{{/if_eq}}
|
|
64
65
|
"react": "17.0.2",
|
|
65
|
-
"react-dom": "17.0.2",{{#
|
|
66
|
+
"react-dom": "17.0.2",{{#if isAppType}}
|
|
66
67
|
"react-router-dom": "^5.2.0",
|
|
67
|
-
"rxjs": "7.8.0",{{/
|
|
68
|
+
"rxjs": "7.8.0",{{/if}}
|
|
68
69
|
"tslib": "2.5.0"
|
|
69
70
|
},
|
|
70
71
|
"packageManager": "{{ packageManagerName }}@{{ packageManagerVersion }}"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Grafana Scenes App Plugin Template
|
|
2
|
+
|
|
3
|
+
This template is a starting point for building an app plugin with [scenes](https://grafana.github.io/scenes) for Grafana.
|
|
4
|
+
|
|
5
|
+
## What are Grafana app plugins?
|
|
6
|
+
|
|
7
|
+
App plugins can let you create a custom out-of-the-box monitoring experience by custom pages, nested datasources and panel plugins.
|
|
8
|
+
|
|
9
|
+
## What is @grafana/scenes
|
|
10
|
+
|
|
11
|
+
[@grafana/scenes](https://github.com/grafana/scenes) is a framework to enable versatile app plugins implementation. It provides an easy way to build apps that resemble Grafana's dashboarding experience, including template variables support, versatile layouts, panels rendering and more.
|
|
12
|
+
|
|
13
|
+
To learn more about @grafana/scenes usage please refer to [documentation](https://grafana.github.io/scenes)
|
|
14
|
+
|
|
15
|
+
## What does this template contain?
|
|
16
|
+
|
|
17
|
+
1. An example of a simple scene. See [Home scene](./src/pages/Home/Home.tsx)
|
|
18
|
+
1. An example of a scene with tabs. See [Scene with tabs](./src/pages/WithTabs/WithTabs.tsx)
|
|
19
|
+
1. An example of a scene with drill down. See [Scene with drill down](./src/pages/WithDrilldown/WithDrilldown.tsx)
|
|
20
|
+
|
|
21
|
+
-- INSERT FRONTEND GETTING STARTED --
|
|
22
|
+
|
|
23
|
+
-- INSERT DISTRIBUTING YOUR PLUGIN --
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { AppRootProps } from '@grafana/data';
|
|
3
|
+
import { PluginPropsContext } from '../../utils/utils.plugin';
|
|
4
|
+
import { Routes } from '../Routes';
|
|
5
|
+
|
|
6
|
+
export class App extends React.PureComponent<AppRootProps> {
|
|
7
|
+
render() {
|
|
8
|
+
return (
|
|
9
|
+
<PluginPropsContext.Provider value={this.props}>
|
|
10
|
+
<Routes />
|
|
11
|
+
</PluginPropsContext.Provider>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './App';
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React, { useState, ChangeEvent } from 'react';
|
|
2
|
+
import { Button, Field, Input, useStyles2, FieldSet } from '@grafana/ui';
|
|
3
|
+
import { PluginConfigPageProps, AppPluginMeta, PluginMeta, GrafanaTheme2 } from '@grafana/data';
|
|
4
|
+
import { getBackendSrv, locationService } from '@grafana/runtime';
|
|
5
|
+
import { css } from '@emotion/css';
|
|
6
|
+
import { SecretInput } from '../SecretInput';
|
|
7
|
+
import { testIds } from '../testIds';
|
|
8
|
+
|
|
9
|
+
export type JsonData = {
|
|
10
|
+
apiUrl?: string;
|
|
11
|
+
isApiKeySet?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type State = {
|
|
15
|
+
// The URL to reach our custom API.
|
|
16
|
+
apiUrl: string;
|
|
17
|
+
// Tells us if the API key secret is set.
|
|
18
|
+
// Set to `true` ONLY if it has already been set and haven't been changed.
|
|
19
|
+
// (We unfortunately need an auxiliray variable for this, as `secureJsonData` is never exposed to the browser after it is set)
|
|
20
|
+
isApiKeySet: boolean;
|
|
21
|
+
// An secret key for our custom API.
|
|
22
|
+
apiKey: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface Props extends PluginConfigPageProps<AppPluginMeta<JsonData>> {}
|
|
26
|
+
|
|
27
|
+
export const AppConfig = ({ plugin }: Props) => {
|
|
28
|
+
const s = useStyles2(getStyles);
|
|
29
|
+
const { enabled, pinned, jsonData } = plugin.meta;
|
|
30
|
+
const [state, setState] = useState<State>({
|
|
31
|
+
apiUrl: jsonData?.apiUrl || '',
|
|
32
|
+
apiKey: '',
|
|
33
|
+
isApiKeySet: Boolean(jsonData?.isApiKeySet),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const onResetApiKey = () =>
|
|
37
|
+
setState({
|
|
38
|
+
...state,
|
|
39
|
+
apiKey: '',
|
|
40
|
+
isApiKeySet: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const onChangeApiKey = (event: ChangeEvent<HTMLInputElement>) => {
|
|
44
|
+
setState({
|
|
45
|
+
...state,
|
|
46
|
+
apiKey: event.target.value.trim(),
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const onChangeApiUrl = (event: ChangeEvent<HTMLInputElement>) => {
|
|
51
|
+
setState({
|
|
52
|
+
...state,
|
|
53
|
+
apiUrl: event.target.value.trim(),
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div data-testid={testIds.appConfig.container}>
|
|
59
|
+
{/* ENABLE / DISABLE PLUGIN */}
|
|
60
|
+
<FieldSet label="Enable / Disable">
|
|
61
|
+
{!enabled && (
|
|
62
|
+
<>
|
|
63
|
+
<div className={s.colorWeak}>The plugin is currently not enabled.</div>
|
|
64
|
+
<Button
|
|
65
|
+
className={s.marginTop}
|
|
66
|
+
variant="primary"
|
|
67
|
+
onClick={() =>
|
|
68
|
+
updatePluginAndReload(plugin.meta.id, {
|
|
69
|
+
enabled: true,
|
|
70
|
+
pinned: true,
|
|
71
|
+
jsonData,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
>
|
|
75
|
+
Enable plugin
|
|
76
|
+
</Button>
|
|
77
|
+
</>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{/* Disable the plugin */}
|
|
81
|
+
{enabled && (
|
|
82
|
+
<>
|
|
83
|
+
<div className={s.colorWeak}>The plugin is currently enabled.</div>
|
|
84
|
+
<Button
|
|
85
|
+
className={s.marginTop}
|
|
86
|
+
variant="destructive"
|
|
87
|
+
onClick={() =>
|
|
88
|
+
updatePluginAndReload(plugin.meta.id, {
|
|
89
|
+
enabled: false,
|
|
90
|
+
pinned: false,
|
|
91
|
+
jsonData,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
Disable plugin
|
|
96
|
+
</Button>
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
</FieldSet>
|
|
100
|
+
|
|
101
|
+
{/* CUSTOM SETTINGS */}
|
|
102
|
+
<FieldSet label="API Settings" className={s.marginTopXl}>
|
|
103
|
+
{/* API Key */}
|
|
104
|
+
<Field label="API Key" description="A secret key for authenticating to our custom API">
|
|
105
|
+
<SecretInput
|
|
106
|
+
width={60}
|
|
107
|
+
data-testid={testIds.appConfig.apiKey}
|
|
108
|
+
id="api-key"
|
|
109
|
+
value={state?.apiKey}
|
|
110
|
+
isConfigured={state.isApiKeySet}
|
|
111
|
+
placeholder={'Your secret API key'}
|
|
112
|
+
onChange={onChangeApiKey}
|
|
113
|
+
onReset={onResetApiKey}
|
|
114
|
+
/>
|
|
115
|
+
</Field>
|
|
116
|
+
|
|
117
|
+
{/* API Url */}
|
|
118
|
+
<Field label="API Url" description="" className={s.marginTop}>
|
|
119
|
+
<Input
|
|
120
|
+
width={60}
|
|
121
|
+
id="api-url"
|
|
122
|
+
data-testid={testIds.appConfig.apiUrl}
|
|
123
|
+
label={`API Url`}
|
|
124
|
+
value={state?.apiUrl}
|
|
125
|
+
placeholder={`E.g.: http://mywebsite.com/api/v1`}
|
|
126
|
+
onChange={onChangeApiUrl}
|
|
127
|
+
/>
|
|
128
|
+
</Field>
|
|
129
|
+
|
|
130
|
+
<div className={s.marginTop}>
|
|
131
|
+
<Button
|
|
132
|
+
type="submit"
|
|
133
|
+
data-testid={testIds.appConfig.submit}
|
|
134
|
+
onClick={() =>
|
|
135
|
+
updatePluginAndReload(plugin.meta.id, {
|
|
136
|
+
enabled,
|
|
137
|
+
pinned,
|
|
138
|
+
jsonData: {
|
|
139
|
+
apiUrl: state.apiUrl,
|
|
140
|
+
isApiKeySet: true,
|
|
141
|
+
},
|
|
142
|
+
// This cannot be queried later by the frontend.
|
|
143
|
+
// We don't want to override it in case it was set previously and left untouched now.
|
|
144
|
+
secureJsonData: state.isApiKeySet
|
|
145
|
+
? undefined
|
|
146
|
+
: {
|
|
147
|
+
apiKey: state.apiKey,
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
disabled={Boolean(!state.apiUrl || (!state.isApiKeySet && !state.apiKey))}
|
|
152
|
+
>
|
|
153
|
+
Save API settings
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
</FieldSet>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const getStyles = (theme: GrafanaTheme2) => ({
|
|
162
|
+
colorWeak: css`
|
|
163
|
+
color: ${theme.colors.text.secondary};
|
|
164
|
+
`,
|
|
165
|
+
marginTop: css`
|
|
166
|
+
margin-top: ${theme.spacing(3)};
|
|
167
|
+
`,
|
|
168
|
+
marginTopXl: css`
|
|
169
|
+
margin-top: ${theme.spacing(6)};
|
|
170
|
+
`,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const updatePluginAndReload = async (pluginId: string, data: Partial<PluginMeta<JsonData>>) => {
|
|
174
|
+
try {
|
|
175
|
+
await updatePlugin(pluginId, data);
|
|
176
|
+
|
|
177
|
+
// Reloading the page as the changes made here wouldn't be propagated to the actual plugin otherwise.
|
|
178
|
+
// This is not ideal, however unfortunately currently there is no supported way for updating the plugin state.
|
|
179
|
+
locationService.reload();
|
|
180
|
+
} catch (e) {
|
|
181
|
+
console.error('Error while updating the plugin', e);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const updatePlugin = async (pluginId: string, data: Partial<PluginMeta>) => {
|
|
186
|
+
const response = await getBackendSrv().datasourceRequest({
|
|
187
|
+
url: `/api/plugins/${pluginId}/settings`,
|
|
188
|
+
method: 'POST',
|
|
189
|
+
data,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return response?.data;
|
|
193
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AppConfig';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Redirect, Route, Switch } from 'react-router-dom';
|
|
3
|
+
import { HomePage } from '../../pages/Home';
|
|
4
|
+
import { PageWithTabs } from '../../pages/WithTabs';
|
|
5
|
+
import { WithDrilldown } from '../../pages/WithDrilldown';
|
|
6
|
+
import { prefixRoute } from '../../utils/utils.routing';
|
|
7
|
+
import { ROUTES } from '../../constants';
|
|
8
|
+
|
|
9
|
+
export const Routes = () => {
|
|
10
|
+
return (
|
|
11
|
+
<Switch>
|
|
12
|
+
<Route path={prefixRoute(`${ROUTES.WithTabs}`)} component={PageWithTabs} />
|
|
13
|
+
<Route path={prefixRoute(`${ROUTES.WithDrilldown}`)} component={WithDrilldown} />
|
|
14
|
+
<Route path={prefixRoute(`${ROUTES.Home}`)} component={HomePage} />
|
|
15
|
+
<Redirect to={prefixRoute(ROUTES.Home)} />
|
|
16
|
+
</Switch>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Routes';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Input, HorizontalGroup, Button } from '@grafana/ui';
|
|
3
|
+
|
|
4
|
+
type Props = React.ComponentProps<typeof Input> & {
|
|
5
|
+
/** Defines if the password is already set or not. (It is needed as in some cases the backend doesn't send back the actual password) */
|
|
6
|
+
isConfigured: boolean;
|
|
7
|
+
/** Called when the user clicks on the "Reset" button */
|
|
8
|
+
onReset: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// This replaces the "LegacyForms.SecretFormField" component from @grafana/ui, so we can start using the newer form components.
|
|
12
|
+
export const SecretInput = ({ isConfigured, onReset, ...props }: Props) => {
|
|
13
|
+
if (isConfigured) {
|
|
14
|
+
return (
|
|
15
|
+
<HorizontalGroup>
|
|
16
|
+
<Input {...props} type="text" disabled={true} value="configured" />
|
|
17
|
+
<Button onClick={onReset} variant="secondary">
|
|
18
|
+
Reset
|
|
19
|
+
</Button>
|
|
20
|
+
</HorizontalGroup>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return <Input {...props} type="password" />;
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SecretInput';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const testIds = {
|
|
2
|
+
appConfig: {
|
|
3
|
+
container: 'data-testid ac-container',
|
|
4
|
+
apiKey: 'data-testid ac-api-key',
|
|
5
|
+
apiUrl: 'data-testid ac-api-url',
|
|
6
|
+
submit: 'data-testid ac-submit-form',
|
|
7
|
+
},
|
|
8
|
+
pageOne: {
|
|
9
|
+
container: 'data-testid pg-one-container',
|
|
10
|
+
navigateToFour: 'data-testid navigate-to-four',
|
|
11
|
+
},
|
|
12
|
+
pageTwo: {
|
|
13
|
+
container: 'data-testid pg-two-container',
|
|
14
|
+
},
|
|
15
|
+
pageThree: {
|
|
16
|
+
container: 'data-testid pg-three-container',
|
|
17
|
+
},
|
|
18
|
+
pageFour: {
|
|
19
|
+
container: 'data-testid pg-four-container',
|
|
20
|
+
navigateBack: 'data-testid navigate-back',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import pluginJson from './plugin.json';
|
|
2
|
+
|
|
3
|
+
export const PLUGIN_BASE_URL = `/a/${pluginJson.id}`;
|
|
4
|
+
|
|
5
|
+
export enum ROUTES {
|
|
6
|
+
Home = 'home',
|
|
7
|
+
WithTabs = 'page-with-tabs',
|
|
8
|
+
WithDrilldown = 'page-with-drilldown',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DATASOURCE_REF = {
|
|
12
|
+
uid: 'gdev-testdata',
|
|
13
|
+
type: 'testdata',
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<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>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AppPlugin } from '@grafana/data';
|
|
2
|
+
import { App } from './components/App';
|
|
3
|
+
import { AppConfig } from './components/AppConfig';
|
|
4
|
+
|
|
5
|
+
export const plugin = new AppPlugin<{}>().setRootPage(App).addConfigPage({
|
|
6
|
+
title: 'Configuration',
|
|
7
|
+
icon: 'cog',
|
|
8
|
+
body: AppConfig,
|
|
9
|
+
id: 'configuration',
|
|
10
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
|
3
|
+
import { Input } from '@grafana/ui';
|
|
4
|
+
|
|
5
|
+
export interface CustomSceneObjectState extends SceneObjectState {
|
|
6
|
+
counter: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class CustomSceneObject extends SceneObjectBase<CustomSceneObjectState> {
|
|
10
|
+
static Component = CustomSceneObjectRenderer;
|
|
11
|
+
|
|
12
|
+
onValueChange = (value: number) => {
|
|
13
|
+
this.setState({ counter: value });
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function CustomSceneObjectRenderer({ model }: SceneComponentProps<CustomSceneObject>) {
|
|
18
|
+
const state = model.useState();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Input
|
|
22
|
+
prefix="Series count"
|
|
23
|
+
defaultValue={state.counter}
|
|
24
|
+
width={20}
|
|
25
|
+
type="number"
|
|
26
|
+
onBlur={(evt) => {
|
|
27
|
+
model.onValueChange(parseInt(evt.currentTarget.value, 10));
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|