@grafana/create-plugin 1.3.1 → 1.4.0-canary.246.66c1931.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 CHANGED
@@ -1,21 +1,3 @@
1
- # v1.3.1 (Mon May 15 2023)
2
-
3
- #### 🐛 Bug Fix
4
-
5
- - Create Plugin: Fix react types errors [#238](https://github.com/grafana/plugin-tools/pull/238) ([@jackw](https://github.com/jackw))
6
- - chore: update grafanaDependency for datasources [#247](https://github.com/grafana/plugin-tools/pull/247) ([@sympatheticmoose](https://github.com/sympatheticmoose))
7
-
8
- #### 📝 Documentation
9
-
10
- - docs: readme update for windows [#240](https://github.com/grafana/plugin-tools/pull/240) ([@sympatheticmoose](https://github.com/sympatheticmoose))
11
-
12
- #### Authors: 2
13
-
14
- - David Harris ([@sympatheticmoose](https://github.com/sympatheticmoose))
15
- - Jack Westbrook ([@jackw](https://github.com/jackw))
16
-
17
- ---
18
-
19
1
  # v1.3.0 (Fri Apr 28 2023)
20
2
 
21
3
  #### 🚀 Enhancement
package/dist/constants.js CHANGED
@@ -30,7 +30,7 @@ var PLUGIN_TYPES;
30
30
  PLUGIN_TYPES["secretsmanager"] = "secretsmanager";
31
31
  })(PLUGIN_TYPES = exports.PLUGIN_TYPES || (exports.PLUGIN_TYPES = {}));
32
32
  exports.EXTRA_TEMPLATE_VARIABLES = {
33
- grafanaVersion: '9.3.8'
33
+ grafanaVersion: '9.5.0'
34
34
  };
35
35
  exports.GRAFANA_FE_PACKAGES = [
36
36
  '@grafana/data',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "1.3.1",
3
+ "version": "1.4.0-canary.246.66c1931.0",
4
4
  "main": "index.js",
5
5
  "repository": {
6
6
  "directory": "packages/create-plugin",
@@ -64,5 +64,5 @@
64
64
  "engines": {
65
65
  "node": ">=16"
66
66
  },
67
- "gitHead": "80d7d3cc8ac542995eae2a874d8b9618f9a9559e"
67
+ "gitHead": "66c1931b45e2c4efcf098ea3a87db432e1a26202"
68
68
  }
package/src/constants.ts CHANGED
@@ -38,7 +38,7 @@ export enum PLUGIN_TYPES {
38
38
  // and will be available to use in the templates.
39
39
  // Example: "@grafana/ui": "{{ grafanaVersion }}"
40
40
  export const EXTRA_TEMPLATE_VARIABLES = {
41
- grafanaVersion: '9.3.8',
41
+ grafanaVersion: '9.5.0',
42
42
  };
43
43
 
44
44
  export const GRAFANA_FE_PACKAGES = [
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { BrowserRouter } from 'react-router-dom';
2
3
  import { AppRootProps, PluginType } from '@grafana/data';
3
4
  import { render, screen } from '@testing-library/react';
4
5
  import { App } from './App';
@@ -25,8 +26,12 @@ describe('Components/App', () => {
25
26
  });
26
27
 
27
28
  test('renders without an error"', () => {
28
- render(<App {...props} />);
29
+ render(
30
+ <BrowserRouter>
31
+ <App {...props} />
32
+ </BrowserRouter>
33
+ );
29
34
 
30
- expect(screen.queryByText(/Hello Grafana!/i)).toBeInTheDocument();
35
+ expect(screen.queryByText(/this is page one./i)).toBeInTheDocument();
31
36
  });
32
37
  });
@@ -1,8 +1,21 @@
1
- import * as React from 'react';
1
+ import React from 'react';
2
+ import { Route, Switch } from 'react-router-dom';
2
3
  import { AppRootProps } from '@grafana/data';
4
+ import { ROUTES } from '../../constants';
5
+ import { PageFour, PageOne, PageThree, PageTwo } from '../../pages';
6
+ import { prefixRoute } from '../../utils/utils.routing';
3
7
 
4
- export class App extends React.PureComponent<AppRootProps> {
5
- render() {
6
- return <div className="page-container">Hello Grafana!</div>;
7
- }
8
+ export function App(props: AppRootProps) {
9
+ return (
10
+ <Switch>
11
+ <Route exact path={prefixRoute(ROUTES.Two)} component={PageTwo} />
12
+ <Route exact path={prefixRoute(`${ROUTES.Three}/:id?`)} component={PageThree} />
13
+
14
+ {/* Full-width page (this page will have no side navigation) */}
15
+ <Route exact path={prefixRoute(ROUTES.Four)} component={PageFour} />
16
+
17
+ {/* Default page */}
18
+ <Route component={PageOne} />
19
+ </Switch>
20
+ );
8
21
  }
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import { PluginType } from '@grafana/data';
4
4
  import { AppConfig, AppConfigProps } from './AppConfig';
5
+ import { testIds } from 'components/testIds';
5
6
 
6
7
  describe('Components/AppConfig', () => {
7
8
  let props: AppConfigProps;
@@ -23,29 +24,15 @@ describe('Components/AppConfig', () => {
23
24
  } as unknown as AppConfigProps;
24
25
  });
25
26
 
26
- test('renders without an error"', () => {
27
- render(<AppConfig plugin={props.plugin} query={props.query} />);
28
-
29
- expect(screen.queryByText(/Enable \/ Disable/i)).toBeInTheDocument();
30
- });
31
-
32
- test('renders an "Enable" button if the plugin is disabled', () => {
27
+ test('renders the "API Settings" fieldset with API key, API url inputs and button', () => {
33
28
  const plugin = { meta: { ...props.plugin.meta, enabled: false } };
34
29
 
35
30
  // @ts-ignore - We don't need to provide `addConfigPage()` and `setChannelSupport()` for these tests
36
31
  render(<AppConfig plugin={plugin} query={props.query} />);
37
32
 
38
- expect(screen.queryByText(/The plugin is currently not enabled./i)).toBeInTheDocument();
39
- expect(screen.queryByText(/The plugin is currently enabled./i)).not.toBeInTheDocument();
40
- });
41
-
42
- test('renders a "Disable" button if the plugin is enabled', () => {
43
- const plugin = { meta: { ...props.plugin.meta, enabled: true } };
44
-
45
- // @ts-ignore - We don't need to provide `addConfigPage()` and `setChannelSupport()` for these tests
46
- render(<AppConfig plugin={plugin} query={props.query} />);
47
-
48
- expect(screen.queryByText(/The plugin is currently enabled./i)).toBeInTheDocument();
49
- expect(screen.queryByText(/The plugin is currently not enabled./i)).not.toBeInTheDocument();
33
+ expect(screen.queryByRole('group', { name: /api settings/i })).toBeInTheDocument();
34
+ expect(screen.queryByTestId(testIds.appConfig.apiKey)).toBeInTheDocument();
35
+ expect(screen.queryByTestId(testIds.appConfig.apiUrl)).toBeInTheDocument();
36
+ expect(screen.queryByRole('button', { name: /save api settings/i })).toBeInTheDocument();
50
37
  });
51
38
  });
@@ -1,62 +1,102 @@
1
- import React from 'react';
2
- import { Button, Legend, useStyles2 } from '@grafana/ui';
3
- import { PluginConfigPageProps, AppPluginMeta, PluginMeta, GrafanaTheme2 } from '@grafana/data';
4
- import { getBackendSrv } from '@grafana/runtime';
5
- import { css } from '@emotion/css';
1
+ import React, { ChangeEvent, useState } from 'react';
6
2
  import { lastValueFrom } from 'rxjs';
3
+ import { css } from '@emotion/css';
4
+ import { AppPluginMeta, GrafanaTheme2, PluginConfigPageProps, PluginMeta } from '@grafana/data';
5
+ import { getBackendSrv } from '@grafana/runtime';
6
+ import { Button, Field, FieldSet, Input, SecretInput, useStyles2 } from '@grafana/ui';
7
+ import { testIds } from '../testIds';
7
8
 
8
- export type AppPluginSettings = {};
9
+ export type AppPluginSettings = {
10
+ apiUrl?: string;
11
+ };
12
+
13
+ type State = {
14
+ // The URL to reach our custom API.
15
+ apiUrl: string;
16
+ // Tells us if the API key secret is set.
17
+ isApiKeySet: boolean;
18
+ // An secret key for our custom API.
19
+ apiKey: string;
20
+ };
9
21
 
10
22
  export interface AppConfigProps extends PluginConfigPageProps<AppPluginMeta<AppPluginSettings>> {}
11
23
 
12
24
  export const AppConfig = ({ plugin }: AppConfigProps) => {
13
25
  const s = useStyles2(getStyles);
14
- const { enabled, jsonData } = plugin.meta;
26
+ const { enabled, pinned, jsonData, secureJsonFields } = plugin.meta;
27
+ const [state, setState] = useState<State>({
28
+ apiUrl: jsonData?.apiUrl || '',
29
+ apiKey: '',
30
+ isApiKeySet: Boolean(secureJsonFields?.apiKey),
31
+ });
32
+
33
+ const onResetApiKey = () =>
34
+ setState({
35
+ ...state,
36
+ apiKey: '',
37
+ isApiKeySet: false,
38
+ });
39
+
40
+ const onChange = (event: ChangeEvent<HTMLInputElement>) => {
41
+ setState({
42
+ ...state,
43
+ [event.target.name]: event.target.value.trim(),
44
+ });
45
+ };
15
46
 
16
47
  return (
17
- <div className="gf-form-group">
18
- <div>
19
- {/* Enable the plugin */}
20
- <Legend>Enable / Disable</Legend>
21
- {!enabled && (
22
- <>
23
- <div className={s.colorWeak}>The plugin is currently not enabled.</div>
24
- <Button
25
- className={s.marginTop}
26
- variant="primary"
27
- onClick={() =>
28
- updatePluginAndReload(plugin.meta.id, {
29
- enabled: true,
30
- pinned: true,
31
- jsonData,
32
- })
33
- }
34
- >
35
- Enable plugin
36
- </Button>
37
- </>
38
- )}
48
+ <div data-testid={testIds.appConfig.container}>
49
+ <FieldSet label="API Settings">
50
+ <Field label="API Key" description="A secret key for authenticating to our custom API">
51
+ <SecretInput
52
+ width={60}
53
+ data-testid={testIds.appConfig.apiKey}
54
+ name="apiKey"
55
+ value={state.apiKey}
56
+ isConfigured={state.isApiKeySet}
57
+ placeholder={'Your secret API key'}
58
+ onChange={onChange}
59
+ onReset={onResetApiKey}
60
+ />
61
+ </Field>
39
62
 
40
- {/* Disable the plugin */}
41
- {enabled && (
42
- <>
43
- <div className={s.colorWeak}>The plugin is currently enabled.</div>
44
- <Button
45
- className={s.marginTop}
46
- variant="destructive"
47
- onClick={() =>
48
- updatePluginAndReload(plugin.meta.id, {
49
- enabled: false,
50
- pinned: false,
51
- jsonData,
52
- })
53
- }
54
- >
55
- Disable plugin
56
- </Button>
57
- </>
58
- )}
59
- </div>
63
+ <Field label="API Url" description="" className={s.marginTop}>
64
+ <Input
65
+ width={60}
66
+ name="apiUrl"
67
+ data-testid={testIds.appConfig.apiUrl}
68
+ value={state.apiUrl}
69
+ placeholder={`E.g.: http://mywebsite.com/api/v1`}
70
+ onChange={onChange}
71
+ />
72
+ </Field>
73
+
74
+ <div className={s.marginTop}>
75
+ <Button
76
+ type="submit"
77
+ data-testid={testIds.appConfig.submit}
78
+ onClick={() =>
79
+ updatePluginAndReload(plugin.meta.id, {
80
+ enabled,
81
+ pinned,
82
+ jsonData: {
83
+ apiUrl: state.apiUrl,
84
+ },
85
+ // This cannot be queried later by the frontend.
86
+ // We don't want to override it in case it was set previously and left untouched now.
87
+ secureJsonData: state.isApiKeySet
88
+ ? undefined
89
+ : {
90
+ apiKey: state.apiKey,
91
+ },
92
+ })
93
+ }
94
+ disabled={Boolean(!state.apiUrl || (!state.isApiKeySet && !state.apiKey))}
95
+ >
96
+ Save API settings
97
+ </Button>
98
+ </div>
99
+ </FieldSet>
60
100
  </div>
61
101
  );
62
102
  };
@@ -68,9 +108,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
68
108
  marginTop: css`
69
109
  margin-top: ${theme.spacing(3)};
70
110
  `,
111
+ marginTopXl: css`
112
+ margin-top: ${theme.spacing(6)};
113
+ `,
71
114
  });
72
115
 
73
- const updatePluginAndReload = async (pluginId: string, data: Partial<PluginMeta>) => {
116
+ const updatePluginAndReload = async (pluginId: string, data: Partial<PluginMeta<AppPluginSettings>>) => {
74
117
  try {
75
118
  await updatePlugin(pluginId, data);
76
119
 
@@ -83,10 +126,11 @@ const updatePluginAndReload = async (pluginId: string, data: Partial<PluginMeta>
83
126
  };
84
127
 
85
128
  export const updatePlugin = async (pluginId: string, data: Partial<PluginMeta>) => {
86
- const response = getBackendSrv().fetch({
129
+ const response = await getBackendSrv().fetch({
87
130
  url: `/api/plugins/${pluginId}/settings`,
88
131
  method: 'POST',
89
132
  data,
90
133
  });
134
+
91
135
  return lastValueFrom(response);
92
136
  };
@@ -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,36 @@
1
+ import { NavModelItem } from '@grafana/data';
2
+ import pluginJson from './plugin.json';
3
+
4
+ export const PLUGIN_BASE_URL = `/a/${pluginJson.id}`;
5
+
6
+ export enum ROUTES {
7
+ One = 'one',
8
+ Two = 'two',
9
+ Three = 'three',
10
+ Four = 'four',
11
+ }
12
+
13
+ export const NAVIGATION_TITLE = 'Basic App Plugin';
14
+ export const NAVIGATION_SUBTITLE = 'Some extra description...';
15
+
16
+ // Add a navigation item for each route you would like to display in the navigation bar
17
+ export const NAVIGATION: Record<string, NavModelItem> = {
18
+ [ROUTES.One]: {
19
+ id: ROUTES.One,
20
+ text: 'Page One',
21
+ icon: 'database',
22
+ url: `${PLUGIN_BASE_URL}/one`,
23
+ },
24
+ [ROUTES.Two]: {
25
+ id: ROUTES.Two,
26
+ text: 'Page Two',
27
+ icon: 'key-skeleton-alt',
28
+ url: `${PLUGIN_BASE_URL}/two`,
29
+ },
30
+ [ROUTES.Three]: {
31
+ id: ROUTES.Three,
32
+ text: 'Page Three',
33
+ icon: 'chart-line',
34
+ url: `${PLUGIN_BASE_URL}/three`,
35
+ },
36
+ };
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { css } from '@emotion/css';
3
+ import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
4
+ import { LinkButton, useStyles2 } from '@grafana/ui';
5
+ import { ROUTES } from '../constants';
6
+ import { prefixRoute } from '../utils/utils.routing';
7
+ import { testIds } from '../components/testIds';
8
+ import { PluginPage } from '@grafana/runtime';
9
+
10
+ export function PageFour() {
11
+ const s = useStyles2(getStyles);
12
+
13
+ return (
14
+ <PluginPage layout={PageLayoutType.Canvas}>
15
+ <div className={s.page} data-testid={testIds.pageFour.container}>
16
+ <div className={s.container}>
17
+ <LinkButton data-testid={testIds.pageFour.navigateBack} icon="arrow-left" href={prefixRoute(ROUTES.One)}>
18
+ Back
19
+ </LinkButton>
20
+ <div className={s.content}>This is a full-width page without a navigation bar.</div>
21
+ </div>
22
+ </div>
23
+ </PluginPage>
24
+ );
25
+ }
26
+
27
+ const getStyles = (theme: GrafanaTheme2) => ({
28
+ page: css`
29
+ padding: ${theme.spacing(3)};
30
+ background-color: ${theme.colors.background.secondary};
31
+ display: flex;
32
+ justify-content: center;
33
+ `,
34
+ container: css`
35
+ width: 900px;
36
+ max-width: 100%;
37
+ min-height: 500px;
38
+ `,
39
+ content: css`
40
+ margin-top: ${theme.spacing(6)};
41
+ `,
42
+ });
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { css } from '@emotion/css';
3
+ import { GrafanaTheme2 } from '@grafana/data';
4
+ import { LinkButton, useStyles2 } from '@grafana/ui';
5
+ import { prefixRoute } from '../utils/utils.routing';
6
+ import { ROUTES } from '../constants';
7
+ import { testIds } from '../components/testIds';
8
+ import { PluginPage } from '@grafana/runtime';
9
+
10
+ export function PageOne() {
11
+ const s = useStyles2(getStyles);
12
+
13
+ return (
14
+ <PluginPage>
15
+ <div data-testid={testIds.pageOne.container}>
16
+ This is page one.
17
+ <div className={s.marginTop}>
18
+ <LinkButton data-testid={testIds.pageOne.navigateToFour} href={prefixRoute(ROUTES.Four)}>
19
+ Full-width page example
20
+ </LinkButton>
21
+ </div>
22
+ </div>
23
+ </PluginPage>
24
+ );
25
+ }
26
+
27
+ const getStyles = (theme: GrafanaTheme2) => ({
28
+ marginTop: css`
29
+ margin-top: ${theme.spacing(2)};
30
+ `,
31
+ });
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { css } from '@emotion/css';
3
+ import { useParams, Link } from 'react-router-dom';
4
+ import { GrafanaTheme2 } from '@grafana/data';
5
+ import { useStyles2 } from '@grafana/ui';
6
+ import { prefixRoute } from '../utils/utils.routing';
7
+ import { ROUTES } from '../constants';
8
+ import { testIds } from '../components/testIds';
9
+ import { PluginPage } from '@grafana/runtime';
10
+
11
+ export function PageThree() {
12
+ const s = useStyles2(getStyles);
13
+ const { id } = useParams<{ id: string }>();
14
+
15
+ return (
16
+ <PluginPage>
17
+ <div data-testid={testIds.pageThree.container}>
18
+ This is page three.
19
+ <br />
20
+ <br />
21
+ {/* The ID parameter is set */}
22
+ {id && (
23
+ <>
24
+ <strong>ID:</strong> {id}
25
+ </>
26
+ )}
27
+ {/* No ID parameter */}
28
+ {!id && (
29
+ <>
30
+ <strong>No id parameter is set in the URL.</strong> <br />
31
+ Try the following link: <br />
32
+ <Link className={s.link} to={prefixRoute(`${ROUTES.Three}/123456789`)}>
33
+ {prefixRoute(`${ROUTES.Three}/123456789`)}
34
+ </Link>
35
+ </>
36
+ )}
37
+ </div>
38
+ </PluginPage>
39
+ );
40
+ }
41
+
42
+ const getStyles = (theme: GrafanaTheme2) => ({
43
+ link: css`
44
+ color: ${theme.colors.text.link};
45
+ text-decoration: underline;
46
+ `,
47
+ });
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { testIds } from '../components/testIds';
3
+ import { PluginPage } from '@grafana/runtime';
4
+
5
+ export function PageTwo() {
6
+ return (
7
+ <PluginPage>
8
+ <div data-testid={testIds.pageTwo.container}>
9
+ <p>This is page two.</p>
10
+ </div>
11
+ </PluginPage>
12
+ );
13
+ }
@@ -0,0 +1,4 @@
1
+ export { PageFour } from './PageFour';
2
+ export { PageOne } from './PageOne';
3
+ export { PageThree } from './PageThree';
4
+ export { PageTwo } from './PageTwo';
@@ -0,0 +1,6 @@
1
+ import { PLUGIN_BASE_URL } from '../constants';
2
+
3
+ // Prefixes the route with the base URL of the plugin
4
+ export function prefixRoute(route: string): string {
5
+ return `${PLUGIN_BASE_URL}/${route}`;
6
+ }
@@ -64,7 +64,7 @@
64
64
  "react": "17.0.2",
65
65
  "react-dom": "17.0.2",{{#if_eq pluginType "app"}}
66
66
  "react-router-dom": "^5.2.0",
67
- "rxjs": "7.5.7",{{/if_eq}}
67
+ "rxjs": "7.8.0",{{/if_eq}}
68
68
  "tslib": "2.5.0"
69
69
  },
70
70
  "packageManager": "{{ packageManagerName }}@{{ packageManagerVersion }}"