@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.
Files changed (35) hide show
  1. package/dist/commands/generate.plopfile.js +7 -6
  2. package/dist/constants.js +2 -0
  3. package/package.json +4 -2
  4. package/scripts/generate-scenes-app.js +24 -0
  5. package/src/commands/generate.plopfile.ts +10 -8
  6. package/src/commands/types.ts +8 -0
  7. package/src/constants.ts +2 -0
  8. package/templates/common/package.json +6 -5
  9. package/templates/scenes-app/README.md +23 -0
  10. package/templates/scenes-app/provisioning/datasources/default.yaml +6 -0
  11. package/templates/scenes-app/src/components/App/App.tsx +14 -0
  12. package/templates/scenes-app/src/components/App/index.tsx +1 -0
  13. package/templates/scenes-app/src/components/AppConfig/AppConfig.tsx +193 -0
  14. package/templates/scenes-app/src/components/AppConfig/index.tsx +1 -0
  15. package/templates/scenes-app/src/components/Routes/Routes.tsx +18 -0
  16. package/templates/scenes-app/src/components/Routes/index.tsx +1 -0
  17. package/templates/scenes-app/src/components/SecretInput/SecretInput.tsx +25 -0
  18. package/templates/scenes-app/src/components/SecretInput/index.tsx +1 -0
  19. package/templates/scenes-app/src/components/testIds.ts +22 -0
  20. package/templates/scenes-app/src/constants.ts +14 -0
  21. package/templates/scenes-app/src/img/logo.svg +1 -0
  22. package/templates/scenes-app/src/module.ts +10 -0
  23. package/templates/scenes-app/src/pages/Home/CustomSceneObject.tsx +31 -0
  24. package/templates/scenes-app/src/pages/Home/Home.tsx +49 -0
  25. package/templates/scenes-app/src/pages/Home/index.tsx +1 -0
  26. package/templates/scenes-app/src/pages/Home/scenes.tsx +100 -0
  27. package/templates/scenes-app/src/pages/WithDrilldown/WithDrilldown.tsx +97 -0
  28. package/templates/scenes-app/src/pages/WithDrilldown/index.tsx +1 -0
  29. package/templates/scenes-app/src/pages/WithDrilldown/panels.tsx +188 -0
  30. package/templates/scenes-app/src/pages/WithDrilldown/scenes.tsx +122 -0
  31. package/templates/scenes-app/src/pages/WithTabs/WithTabs.tsx +45 -0
  32. package/templates/scenes-app/src/pages/WithTabs/index.tsx +1 -0
  33. package/templates/scenes-app/src/plugin.json +54 -0
  34. package/templates/scenes-app/src/utils/utils.plugin.ts +17 -0
  35. 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, _b = _a.templateData, templateData = _b === void 0 ? {} : _b;
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": "bd765873e928d1c305627e3b7224f3408db42863"
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: Record<string, string>;
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: Record<string, string> = {}
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?: Record<string, string>;
225
+ templateData: TemplateData;
224
226
  }) {
225
227
  let files = glob.sync(`${folderPath}/**`, { dot: true });
226
228
 
@@ -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",{{#if_eq pluginType "app"}}
34
- "@types/react-router-dom": "^5.3.3",{{/if_eq}}
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",{{#if_eq pluginType "app"}}
66
+ "react-dom": "17.0.2",{{#if isAppType}}
66
67
  "react-router-dom": "^5.2.0",
67
- "rxjs": "7.8.0",{{/if_eq}}
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,6 @@
1
+ apiVersion: 1
2
+
3
+ datasources:
4
+ - name: gdev-testdata
5
+ isDefault: true
6
+ type: testdata
@@ -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
+ }