@grafana/create-plugin 1.4.0 → 1.4.1-canary.250.5b7466f.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 +9 -7
- 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 +12 -9
- 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
|
{
|
|
@@ -100,11 +100,13 @@ function default_1(plop) {
|
|
|
100
100
|
var pluginId = (0, utils_handlebars_1.normalizeId)(pluginName, orgName, pluginType);
|
|
101
101
|
var _b = (0, utils_packageManager_1.getPackageManagerFromUserAgent)(), packageManagerName = _b.packageManagerName, packageManagerVersion = _b.packageManagerVersion;
|
|
102
102
|
var packageManagerInstallCmd = (0, utils_packageManager_1.getPackageManagerInstallCmd)(packageManagerName);
|
|
103
|
+
var isAppType = pluginType === constants_1.PLUGIN_TYPES.app || pluginType === constants_1.PLUGIN_TYPES.scenes;
|
|
103
104
|
var templateData = {
|
|
104
105
|
pluginId: pluginId,
|
|
105
106
|
packageManagerName: packageManagerName,
|
|
106
107
|
packageManagerInstallCmd: packageManagerInstallCmd,
|
|
107
|
-
packageManagerVersion: packageManagerVersion
|
|
108
|
+
packageManagerVersion: packageManagerVersion,
|
|
109
|
+
isAppType: isAppType
|
|
108
110
|
};
|
|
109
111
|
var commonActions = getActionsForTemplateFolder({
|
|
110
112
|
folderPath: constants_1.TEMPLATE_PATHS.common,
|
|
@@ -113,11 +115,12 @@ function default_1(plop) {
|
|
|
113
115
|
});
|
|
114
116
|
var pluginTypeSpecificActions = getActionsForTemplateFolder({
|
|
115
117
|
folderPath: constants_1.TEMPLATE_PATHS[pluginType],
|
|
116
|
-
exportPath: exportPath
|
|
118
|
+
exportPath: exportPath,
|
|
119
|
+
templateData: templateData
|
|
117
120
|
});
|
|
118
|
-
var backendFolderPath =
|
|
121
|
+
var backendFolderPath = isAppType ? constants_1.TEMPLATE_PATHS.backendApp : constants_1.TEMPLATE_PATHS.backend;
|
|
119
122
|
var backendActions = hasBackend
|
|
120
|
-
? getActionsForTemplateFolder({ folderPath: backendFolderPath, exportPath: exportPath })
|
|
123
|
+
? getActionsForTemplateFolder({ folderPath: backendFolderPath, exportPath: exportPath, templateData: templateData })
|
|
121
124
|
: [];
|
|
122
125
|
var pluginActions = __spreadArray(__spreadArray(__spreadArray([], backendActions, true), pluginTypeSpecificActions, true), commonActions, true).reduce(function (acc, file) {
|
|
123
126
|
var actionExists = acc.find(function (f) { return f.path === file.path; });
|
|
@@ -163,7 +166,6 @@ function getActionsForReadme(_a) {
|
|
|
163
166
|
];
|
|
164
167
|
}
|
|
165
168
|
function replacePatternWithTemplateInReadme(pattern, partialsFile, exportPath, templateData) {
|
|
166
|
-
if (templateData === void 0) { templateData = {}; }
|
|
167
169
|
return {
|
|
168
170
|
type: 'modify',
|
|
169
171
|
path: path_1["default"].join(exportPath, 'README.md'),
|
|
@@ -174,7 +176,7 @@ function replacePatternWithTemplateInReadme(pattern, partialsFile, exportPath, t
|
|
|
174
176
|
};
|
|
175
177
|
}
|
|
176
178
|
function getActionsForTemplateFolder(_a) {
|
|
177
|
-
var folderPath = _a.folderPath, exportPath = _a.exportPath,
|
|
179
|
+
var folderPath = _a.folderPath, exportPath = _a.exportPath, templateData = _a.templateData;
|
|
178
180
|
var files = glob_1["default"].sync("".concat(folderPath, "/**"), { dot: true });
|
|
179
181
|
if (templateData.packageManagerName !== 'pnpm') {
|
|
180
182
|
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.5b7466f.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": "5b7466f9383e929fb1efd68aae3c29e4e64db20e"
|
|
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,13 @@ 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
|
|
91
|
+
const isAppType = pluginType === PLUGIN_TYPES.app || pluginType === PLUGIN_TYPES.scenes;
|
|
92
|
+
const templateData: TemplateData = {
|
|
92
93
|
pluginId,
|
|
93
94
|
packageManagerName,
|
|
94
95
|
packageManagerInstallCmd,
|
|
95
96
|
packageManagerVersion,
|
|
97
|
+
isAppType,
|
|
96
98
|
};
|
|
97
99
|
// Copy over files that are shared between plugins types
|
|
98
100
|
const commonActions = getActionsForTemplateFolder({
|
|
@@ -105,12 +107,13 @@ export default function (plop: NodePlopAPI) {
|
|
|
105
107
|
const pluginTypeSpecificActions = getActionsForTemplateFolder({
|
|
106
108
|
folderPath: TEMPLATE_PATHS[pluginType],
|
|
107
109
|
exportPath,
|
|
110
|
+
templateData,
|
|
108
111
|
});
|
|
109
112
|
|
|
110
113
|
// Copy over backend-specific files (if selected)
|
|
111
|
-
const backendFolderPath =
|
|
114
|
+
const backendFolderPath = isAppType ? TEMPLATE_PATHS.backendApp : TEMPLATE_PATHS.backend;
|
|
112
115
|
const backendActions = hasBackend
|
|
113
|
-
? getActionsForTemplateFolder({ folderPath: backendFolderPath, exportPath })
|
|
116
|
+
? getActionsForTemplateFolder({ folderPath: backendFolderPath, exportPath, templateData })
|
|
114
117
|
: [];
|
|
115
118
|
|
|
116
119
|
// Common, pluginType and backend actions can contain different templates for the same destination.
|
|
@@ -168,7 +171,7 @@ function getActionsForReadme({
|
|
|
168
171
|
templateData,
|
|
169
172
|
}: {
|
|
170
173
|
exportPath: string;
|
|
171
|
-
templateData:
|
|
174
|
+
templateData: TemplateData;
|
|
172
175
|
}): ModifyActionConfig[] {
|
|
173
176
|
return [
|
|
174
177
|
replacePatternWithTemplateInReadme(
|
|
@@ -196,7 +199,7 @@ function replacePatternWithTemplateInReadme(
|
|
|
196
199
|
pattern: string,
|
|
197
200
|
partialsFile: string,
|
|
198
201
|
exportPath: string,
|
|
199
|
-
templateData:
|
|
202
|
+
templateData: TemplateData
|
|
200
203
|
): ModifyActionConfig {
|
|
201
204
|
return {
|
|
202
205
|
type: 'modify',
|
|
@@ -216,11 +219,11 @@ function replacePatternWithTemplateInReadme(
|
|
|
216
219
|
function getActionsForTemplateFolder({
|
|
217
220
|
folderPath,
|
|
218
221
|
exportPath,
|
|
219
|
-
templateData
|
|
222
|
+
templateData,
|
|
220
223
|
}: {
|
|
221
224
|
folderPath: string;
|
|
222
225
|
exportPath: string;
|
|
223
|
-
templateData
|
|
226
|
+
templateData: TemplateData;
|
|
224
227
|
}) {
|
|
225
228
|
let files = glob.sync(`${folderPath}/**`, { dot: true });
|
|
226
229
|
|
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
|
+
}
|