Skip to content

Commit

Permalink
Create Plugin: Code split scaffolded apps (#1072)
Browse files Browse the repository at this point in the history
  • Loading branch information
jackw authored Sep 5, 2024
1 parent 853c2e9 commit 7675426
Show file tree
Hide file tree
Showing 32 changed files with 124 additions and 76 deletions.
3 changes: 3 additions & 0 deletions docusaurus/docs/get-started/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ Is something missing from this list? [Let us know](https://github.com/grafana/pl
## App plugins

- **Specify a root page for your app** - If your app defines multiple pages, make sure to select a default one that will be used as a landing page for your plugin.
- **Code split your app** - If you app contains multiple pages, make sure to use code splitting techniques to improve frontend load performance. By default Webpack will display warnings in terminal during build if any frontend assets are larger than 250kb. See the following links for more info:
- [SurviveJs code splitting overview](https://survivejs.com/books/webpack/building/code-splitting)
- [Official React lazy documentation](https://react.dev/reference/react/lazy)
- **To generate dynamic apps, consider using [Grafana Scenes](https://grafana.com/developers/scenes/).**
- **Consider contributing a [UI extension](../key-concepts/ui-extensions)** - UI extensions can help a user to discover your app in context and continue a given workflow. Additionally, if your app provides context that can be used in other apps, then create an extension point to allow these apps to do so, with no further changes required in your app.

Expand Down
2 changes: 1 addition & 1 deletion docusaurus/docs/reference/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The `plugin.json` file is required for all plugins. When Grafana starts, it scan
| `includes` | [object](#includes)[] | No | Resources to include in plugin. |
| `logs` | boolean | No | For data source plugins, if the plugin supports logs. It may be used to filter logs only features. |
| `metrics` | boolean | No | For data source plugins, if the plugin supports metric queries. Used to enable the plugin in the panel editor. |
| `preload` | boolean | No | Initialize plugin on startup. By default, the plugin initializes on first use, but when preload is set to true the plugin loads when the Grafana web app loads the first time. Only applicable to app plugins. |
| `preload` | boolean | No | Initialize plugin on startup. By default, the plugin initializes on first use, but when preload is set to true the plugin loads when the Grafana web app loads the first time. Only applicable to app plugins. When setting to true implement [frontend code splitting](../get-started/best-practices.md#app-plugins) to minimise performance implications. |
| `queryOptions` | [object](#queryoptions) | No | For data source plugins. There is a query options section in the plugin's query editor and these options can be turned on if needed. |
| `roles` | [object](#roles)[] | No | List of RBAC roles and their default assignments. |
| `routes` | [object](#routes)[] | No | For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins](../how-to-guides/data-source-plugins/add-authentication-for-data-source-plugins). |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { AppRootProps, PluginType } from '@grafana/data';
import { render, screen } from '@testing-library/react';
import { App } from './App';
import App from './App';

describe('Components/App', () => {
let props: AppRootProps;
Expand All @@ -25,13 +25,13 @@ describe('Components/App', () => {
} as unknown as AppRootProps;
});

test('renders without an error"', () => {
test('renders without an error"', async () => {
render(
<BrowserRouter>
<App {...props} />
</BrowserRouter>
);

expect(screen.queryByText(/this is page one./i)).toBeInTheDocument();
expect(await screen.findByText(/this is page one./i)).toBeInTheDocument();
});
});
25 changes: 15 additions & 10 deletions packages/create-plugin/templates/app/src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@ import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { AppRootProps } from '@grafana/data';
import { ROUTES } from '../../constants';
import { PageFour, PageOne, PageThree, PageTwo } from '../../pages';
const PageOne = React.lazy(() => import('../../pages/PageOne'));
const PageTwo = React.lazy(() => import('../../pages/PageTwo'));
const PageThree = React.lazy(() => import('../../pages/PageThree'));
const PageFour = React.lazy(() => import('../../pages/PageFour'));

export function App(props: AppRootProps) {
function App(props: AppRootProps) {
return (
<Routes>
<Route path={ROUTES.Two} element={<PageTwo />} />
<Route path={`${ROUTES.Three}/:id?`} element={<PageThree />} />
<Routes>
<Route path={ROUTES.Two} element={<PageTwo />} />
<Route path={`${ROUTES.Three}/:id?`} element={<PageThree />} />

{/* Full-width page (this page will have no side navigation) */}
<Route path={ROUTES.Four} element={<PageFour />} />
{/* Full-width page (this page will have no side navigation) */}
<Route path={ROUTES.Four} element={<PageFour />} />

{/* Default page */}
<Route path="*" element={<PageOne />} />
</Routes>
{/* Default page */}
<Route path="*" element={<PageOne />} />
</Routes>
);
}

export default App;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { PluginType } from '@grafana/data';
import { AppConfig, AppConfigProps } from './AppConfig';
import AppConfig, { AppConfigProps } from './AppConfig';
import { testIds } from 'components/testIds';

describe('Components/AppConfig', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type State = {

export interface AppConfigProps extends PluginConfigPageProps<AppPluginMeta<AppPluginSettings>> {}

export const AppConfig = ({ plugin }: AppConfigProps) => {
const AppConfig = ({ plugin }: AppConfigProps) => {
const s = useStyles2(getStyles);
const { enabled, pinned, jsonData, secureJsonFields } = plugin.meta;
const [state, setState] = useState<State>({
Expand Down Expand Up @@ -110,6 +110,8 @@ export const AppConfig = ({ plugin }: AppConfigProps) => {
);
};

export default AppConfig;

const getStyles = (theme: GrafanaTheme2) => ({
colorWeak: css`
color: ${theme.colors.text.secondary};
Expand Down

This file was deleted.

10 changes: 0 additions & 10 deletions packages/create-plugin/templates/app/src/module.ts

This file was deleted.

26 changes: 26 additions & 0 deletions packages/create-plugin/templates/app/src/module.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { Suspense, lazy } from 'react';
import { AppPlugin, type AppRootProps } from '@grafana/data';
import { LoadingPlaceholder } from '@grafana/ui';
import type { AppConfigProps } from './components/AppConfig/AppConfig';

const LazyApp = lazy(() => import('./components/App/App'));
const LazyAppConfig = lazy(() => import('./components/AppConfig/AppConfig'));

const App = (props: AppRootProps) => (
<Suspense fallback={<LoadingPlaceholder text="" />}>
<LazyApp {...props} />
</Suspense>
);

const AppConfig = (props: AppConfigProps) => (
<Suspense fallback={<LoadingPlaceholder text="" />}>
<LazyAppConfig {...props} />
</Suspense>
);

export const plugin = new AppPlugin<{}>().setRootPage(App).addConfigPage({
title: 'Configuration',
icon: 'cog',
body: AppConfig,
id: 'configuration',
});
4 changes: 3 additions & 1 deletion packages/create-plugin/templates/app/src/pages/PageFour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { prefixRoute } from '../utils/utils.routing';
import { testIds } from '../components/testIds';
import { PluginPage } from '@grafana/runtime';

export function PageFour() {
function PageFour() {
const s = useStyles2(getStyles);

return (
Expand All @@ -24,6 +24,8 @@ export function PageFour() {
);
}

export default PageFour;

const getStyles = (theme: GrafanaTheme2) => ({
page: css`
padding: ${theme.spacing(3)};
Expand Down
4 changes: 3 additions & 1 deletion packages/create-plugin/templates/app/src/pages/PageOne.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ROUTES } from '../constants';
import { testIds } from '../components/testIds';
import { PluginPage } from '@grafana/runtime';

export function PageOne() {
function PageOne() {
const s = useStyles2(getStyles);

return (
Expand All @@ -24,6 +24,8 @@ export function PageOne() {
);
}

export default PageOne;

const getStyles = (theme: GrafanaTheme2) => ({
marginTop: css`
margin-top: ${theme.spacing(2)};
Expand Down
4 changes: 3 additions & 1 deletion packages/create-plugin/templates/app/src/pages/PageThree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ROUTES } from '../constants';
import { testIds } from '../components/testIds';
import { PluginPage } from '@grafana/runtime';

export function PageThree() {
function PageThree() {
const s = useStyles2(getStyles);
const { id } = useParams<{ id: string }>();

Expand Down Expand Up @@ -39,6 +39,8 @@ export function PageThree() {
);
}

export default PageThree;

const getStyles = (theme: GrafanaTheme2) => ({
link: css`
color: ${theme.colors.text.link};
Expand Down
4 changes: 3 additions & 1 deletion packages/create-plugin/templates/app/src/pages/PageTwo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { testIds } from '../components/testIds';
import { PluginPage } from '@grafana/runtime';

export function PageTwo() {
function PageTwo() {
return (
<PluginPage>
<div data-testid={testIds.pageTwo.container}>
Expand All @@ -11,3 +11,5 @@ export function PageTwo() {
</PluginPage>
);
}

export default PageTwo;
4 changes: 0 additions & 4 deletions packages/create-plugin/templates/app/src/pages/index.tsx

This file was deleted.

6 changes: 3 additions & 3 deletions packages/create-plugin/templates/common/.config/jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

import '@testing-library/jest-dom';
import { TextEncoder, TextDecoder } from 'util';

Object.assign(global, { TextDecoder, TextEncoder });

// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(global, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
value: (query) => ({
matches: false,
media: query,
onchange: null,
Expand All @@ -22,7 +22,7 @@ Object.defineProperty(global, 'matchMedia', {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
}),
});

HTMLCanvasElement.prototype.getContext = () => {};
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import { AppRootProps } from '@grafana/data';
import { PluginPropsContext } from '../../utils/utils.plugin';
import { Routes } from '../Routes';
import { Routes } from '../Routes/Routes';

export class App extends React.PureComponent<AppRootProps> {
render() {
return (
<PluginPropsContext.Provider value={this.props}>
<Routes />
</PluginPropsContext.Provider>
);
}
function App(props: AppRootProps) {
return (
<PluginPropsContext.Provider value={props}>
<Routes />
</PluginPropsContext.Provider>
);
}

export default App;

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ type State = {
apiKey: string;
};

interface Props extends PluginConfigPageProps<AppPluginMeta<JsonData>> {}
export interface AppConfigProps extends PluginConfigPageProps<AppPluginMeta<JsonData>> {}

export const AppConfig = ({ plugin }: Props) => {
const AppConfig = ({ plugin }: AppConfigProps) => {
const s = useStyles2(getStyles);
const { enabled, pinned, jsonData } = plugin.meta;
const [state, setState] = useState<State>({
Expand Down Expand Up @@ -114,6 +114,8 @@ export const AppConfig = ({ plugin }: Props) => {
);
};

export default AppConfig;

const getStyles = (theme: GrafanaTheme2) => ({
colorWeak: css`
color: ${theme.colors.text.secondary};
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { HomePage } from '../../pages/Home';
import { PageWithTabs } from '../../pages/WithTabs';
import { WithDrilldown } from '../../pages/WithDrilldown';
import { prefixRoute } from '../../utils/utils.routing';
import { ROUTES } from '../../constants';
import { HelloWorldPluginPage } from '../../pages/HelloWorld';
const HomePage = React.lazy(() => import('../../pages/Home/Home'));
const PageWithTabs = React.lazy(() => import('../../pages/WithTabs/WithTabs'));
const WithDrilldown = React.lazy(() => import('../../pages/WithDrilldown/WithDrilldown'));
const HelloWorld = React.lazy(() => import('../../pages/HelloWorld/HelloWorld'));

export const Routes = () => {
return (
<Switch>
<Route path={prefixRoute(`${ROUTES.WithTabs}`)} component={PageWithTabs} />
<Route path={prefixRoute(`${ROUTES.WithDrilldown}`)} component={WithDrilldown} />
<Route path={prefixRoute(`${ROUTES.Home}`)} component={HomePage} />
<Route path={prefixRoute(`${ROUTES.HelloWorld}`)} component={HelloWorldPluginPage} />
<Route path={prefixRoute(`${ROUTES.HelloWorld}`)} component={HelloWorld} />
<Redirect to={prefixRoute(ROUTES.Home)} />
</Switch>
);
Expand Down

This file was deleted.

10 changes: 0 additions & 10 deletions packages/create-plugin/templates/scenes-app/src/module.ts

This file was deleted.

26 changes: 26 additions & 0 deletions packages/create-plugin/templates/scenes-app/src/module.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { Suspense, lazy } from 'react';
import { AppPlugin, type AppRootProps } from '@grafana/data';
import { LoadingPlaceholder } from '@grafana/ui';
import type { AppConfigProps } from './components/AppConfig/AppConfig';

const LazyApp = lazy(() => import('./components/App/App'));
const LazyAppConfig = lazy(() => import('./components/AppConfig/AppConfig'));

const App = (props: AppRootProps) => (
<Suspense fallback={<LoadingPlaceholder text="" />}>
<LazyApp {...props} />
</Suspense>
);

const AppConfig = (props: AppConfigProps) => (
<Suspense fallback={<LoadingPlaceholder text="" />}>
<LazyAppConfig {...props} />
</Suspense>
);

export const plugin = new AppPlugin<{}>().setRootPage(App).addConfigPage({
title: 'Configuration',
icon: 'cog',
body: AppConfig,
id: 'configuration',
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react';
import { getScene } from './helloWorldScene';

export const HelloWorldPluginPage = () => {
const HelloWorld = () => {
const scene = getScene();

return <scene.Component model={scene} />;
};

export default HelloWorld;

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const getScene = () => {
],
});
};
export const HomePage = () => {

const HomePage = () => {
const scene = useMemo(() => getScene(), []);

return (
Expand All @@ -47,3 +48,5 @@ export const HomePage = () => {
</>
);
};

export default HomePage;

This file was deleted.

Loading

0 comments on commit 7675426

Please sign in to comment.