diff --git a/docusaurus/docs/get-started/best-practices.md b/docusaurus/docs/get-started/best-practices.md index a558a8778..cdbad0199 100644 --- a/docusaurus/docs/get-started/best-practices.md +++ b/docusaurus/docs/get-started/best-practices.md @@ -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. diff --git a/docusaurus/docs/reference/metadata.md b/docusaurus/docs/reference/metadata.md index f36a26429..bd952358b 100644 --- a/docusaurus/docs/reference/metadata.md +++ b/docusaurus/docs/reference/metadata.md @@ -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). | diff --git a/packages/create-plugin/templates/app/src/components/App/App.test.tsx b/packages/create-plugin/templates/app/src/components/App/App.test.tsx index e2cc836f7..6d59f3a9e 100644 --- a/packages/create-plugin/templates/app/src/components/App/App.test.tsx +++ b/packages/create-plugin/templates/app/src/components/App/App.test.tsx @@ -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; @@ -25,13 +25,13 @@ describe('Components/App', () => { } as unknown as AppRootProps; }); - test('renders without an error"', () => { + test('renders without an error"', async () => { render( ); - expect(screen.queryByText(/this is page one./i)).toBeInTheDocument(); + expect(await screen.findByText(/this is page one./i)).toBeInTheDocument(); }); }); diff --git a/packages/create-plugin/templates/app/src/components/App/App.tsx b/packages/create-plugin/templates/app/src/components/App/App.tsx index 043ab20ee..995ec587c 100644 --- a/packages/create-plugin/templates/app/src/components/App/App.tsx +++ b/packages/create-plugin/templates/app/src/components/App/App.tsx @@ -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 ( - - } /> - } /> + + } /> + } /> - {/* Full-width page (this page will have no side navigation) */} - } /> + {/* Full-width page (this page will have no side navigation) */} + } /> - {/* Default page */} - } /> - + {/* Default page */} + } /> + ); } + +export default App; diff --git a/packages/create-plugin/templates/app/src/components/App/index.tsx b/packages/create-plugin/templates/app/src/components/App/index.tsx deleted file mode 100644 index ac7ba3b3a..000000000 --- a/packages/create-plugin/templates/app/src/components/App/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './App'; diff --git a/packages/create-plugin/templates/app/src/components/AppConfig/AppConfig.test.tsx b/packages/create-plugin/templates/app/src/components/AppConfig/AppConfig.test.tsx index 3678d28a1..604662009 100644 --- a/packages/create-plugin/templates/app/src/components/AppConfig/AppConfig.test.tsx +++ b/packages/create-plugin/templates/app/src/components/AppConfig/AppConfig.test.tsx @@ -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', () => { diff --git a/packages/create-plugin/templates/app/src/components/AppConfig/AppConfig.tsx b/packages/create-plugin/templates/app/src/components/AppConfig/AppConfig.tsx index 19fba6438..075124221 100644 --- a/packages/create-plugin/templates/app/src/components/AppConfig/AppConfig.tsx +++ b/packages/create-plugin/templates/app/src/components/AppConfig/AppConfig.tsx @@ -21,7 +21,7 @@ type State = { export interface AppConfigProps extends PluginConfigPageProps> {} -export const AppConfig = ({ plugin }: AppConfigProps) => { +const AppConfig = ({ plugin }: AppConfigProps) => { const s = useStyles2(getStyles); const { enabled, pinned, jsonData, secureJsonFields } = plugin.meta; const [state, setState] = useState({ @@ -110,6 +110,8 @@ export const AppConfig = ({ plugin }: AppConfigProps) => { ); }; +export default AppConfig; + const getStyles = (theme: GrafanaTheme2) => ({ colorWeak: css` color: ${theme.colors.text.secondary}; diff --git a/packages/create-plugin/templates/app/src/components/AppConfig/index.tsx b/packages/create-plugin/templates/app/src/components/AppConfig/index.tsx deleted file mode 100644 index 1dba18f08..000000000 --- a/packages/create-plugin/templates/app/src/components/AppConfig/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './AppConfig'; diff --git a/packages/create-plugin/templates/app/src/module.ts b/packages/create-plugin/templates/app/src/module.ts deleted file mode 100644 index 319808b1b..000000000 --- a/packages/create-plugin/templates/app/src/module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AppPlugin } from '@grafana/data'; -import { App } from './components/App'; -import { AppConfig } from './components/AppConfig'; - -export const plugin = new AppPlugin<{}>().setRootPage(App).addConfigPage({ - title: 'Configuration', - icon: 'cog', - body: AppConfig, - id: 'configuration', -}); diff --git a/packages/create-plugin/templates/app/src/module.tsx b/packages/create-plugin/templates/app/src/module.tsx new file mode 100644 index 000000000..33fb72bd2 --- /dev/null +++ b/packages/create-plugin/templates/app/src/module.tsx @@ -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) => ( + }> + + +); + +const AppConfig = (props: AppConfigProps) => ( + }> + + +); + +export const plugin = new AppPlugin<{}>().setRootPage(App).addConfigPage({ + title: 'Configuration', + icon: 'cog', + body: AppConfig, + id: 'configuration', +}); diff --git a/packages/create-plugin/templates/app/src/pages/PageFour.tsx b/packages/create-plugin/templates/app/src/pages/PageFour.tsx index 359a5dce3..b4057159b 100644 --- a/packages/create-plugin/templates/app/src/pages/PageFour.tsx +++ b/packages/create-plugin/templates/app/src/pages/PageFour.tsx @@ -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 ( @@ -24,6 +24,8 @@ export function PageFour() { ); } +export default PageFour; + const getStyles = (theme: GrafanaTheme2) => ({ page: css` padding: ${theme.spacing(3)}; diff --git a/packages/create-plugin/templates/app/src/pages/PageOne.tsx b/packages/create-plugin/templates/app/src/pages/PageOne.tsx index 0b95eea3a..b219accbb 100644 --- a/packages/create-plugin/templates/app/src/pages/PageOne.tsx +++ b/packages/create-plugin/templates/app/src/pages/PageOne.tsx @@ -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 ( @@ -24,6 +24,8 @@ export function PageOne() { ); } +export default PageOne; + const getStyles = (theme: GrafanaTheme2) => ({ marginTop: css` margin-top: ${theme.spacing(2)}; diff --git a/packages/create-plugin/templates/app/src/pages/PageThree.tsx b/packages/create-plugin/templates/app/src/pages/PageThree.tsx index 6d2eff3f4..db44a07c9 100644 --- a/packages/create-plugin/templates/app/src/pages/PageThree.tsx +++ b/packages/create-plugin/templates/app/src/pages/PageThree.tsx @@ -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 }>(); @@ -39,6 +39,8 @@ export function PageThree() { ); } +export default PageThree; + const getStyles = (theme: GrafanaTheme2) => ({ link: css` color: ${theme.colors.text.link}; diff --git a/packages/create-plugin/templates/app/src/pages/PageTwo.tsx b/packages/create-plugin/templates/app/src/pages/PageTwo.tsx index 16ade6711..1360eb6ba 100644 --- a/packages/create-plugin/templates/app/src/pages/PageTwo.tsx +++ b/packages/create-plugin/templates/app/src/pages/PageTwo.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { testIds } from '../components/testIds'; import { PluginPage } from '@grafana/runtime'; -export function PageTwo() { +function PageTwo() { return (
@@ -11,3 +11,5 @@ export function PageTwo() { ); } + +export default PageTwo; diff --git a/packages/create-plugin/templates/app/src/pages/index.tsx b/packages/create-plugin/templates/app/src/pages/index.tsx deleted file mode 100644 index 519092d2e..000000000 --- a/packages/create-plugin/templates/app/src/pages/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export { PageFour } from './PageFour'; -export { PageOne } from './PageOne'; -export { PageThree } from './PageThree'; -export { PageTwo } from './PageTwo'; diff --git a/packages/create-plugin/templates/common/.config/jest-setup.js b/packages/create-plugin/templates/common/.config/jest-setup.js index d0629b471..74832e34f 100644 --- a/packages/create-plugin/templates/common/.config/jest-setup.js +++ b/packages/create-plugin/templates/common/.config/jest-setup.js @@ -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, @@ -22,7 +22,7 @@ Object.defineProperty(global, 'matchMedia', { addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), - })), + }), }); HTMLCanvasElement.prototype.getContext = () => {}; diff --git a/packages/create-plugin/templates/scenes-app/src/components/App/App.tsx b/packages/create-plugin/templates/scenes-app/src/components/App/App.tsx index f79f7335a..567acdfe1 100644 --- a/packages/create-plugin/templates/scenes-app/src/components/App/App.tsx +++ b/packages/create-plugin/templates/scenes-app/src/components/App/App.tsx @@ -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 { - render() { - return ( - - - - ); - } +function App(props: AppRootProps) { + return ( + + + + ); } + +export default App; diff --git a/packages/create-plugin/templates/scenes-app/src/components/App/index.tsx b/packages/create-plugin/templates/scenes-app/src/components/App/index.tsx deleted file mode 100644 index ac7ba3b3a..000000000 --- a/packages/create-plugin/templates/scenes-app/src/components/App/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './App'; diff --git a/packages/create-plugin/templates/scenes-app/src/components/AppConfig/AppConfig.tsx b/packages/create-plugin/templates/scenes-app/src/components/AppConfig/AppConfig.tsx index 5d324ffd9..fe3bedc44 100644 --- a/packages/create-plugin/templates/scenes-app/src/components/AppConfig/AppConfig.tsx +++ b/packages/create-plugin/templates/scenes-app/src/components/AppConfig/AppConfig.tsx @@ -22,9 +22,9 @@ type State = { apiKey: string; }; -interface Props extends PluginConfigPageProps> {} +export interface AppConfigProps extends PluginConfigPageProps> {} -export const AppConfig = ({ plugin }: Props) => { +const AppConfig = ({ plugin }: AppConfigProps) => { const s = useStyles2(getStyles); const { enabled, pinned, jsonData } = plugin.meta; const [state, setState] = useState({ @@ -114,6 +114,8 @@ export const AppConfig = ({ plugin }: Props) => { ); }; +export default AppConfig; + const getStyles = (theme: GrafanaTheme2) => ({ colorWeak: css` color: ${theme.colors.text.secondary}; diff --git a/packages/create-plugin/templates/scenes-app/src/components/AppConfig/index.tsx b/packages/create-plugin/templates/scenes-app/src/components/AppConfig/index.tsx deleted file mode 100644 index 1dba18f08..000000000 --- a/packages/create-plugin/templates/scenes-app/src/components/AppConfig/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './AppConfig'; diff --git a/packages/create-plugin/templates/scenes-app/src/components/Routes/Routes.tsx b/packages/create-plugin/templates/scenes-app/src/components/Routes/Routes.tsx index 017753e04..5a18a78e3 100644 --- a/packages/create-plugin/templates/scenes-app/src/components/Routes/Routes.tsx +++ b/packages/create-plugin/templates/scenes-app/src/components/Routes/Routes.tsx @@ -1,11 +1,11 @@ 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 ( @@ -13,7 +13,7 @@ export const Routes = () => { - + ); diff --git a/packages/create-plugin/templates/scenes-app/src/components/Routes/index.tsx b/packages/create-plugin/templates/scenes-app/src/components/Routes/index.tsx deleted file mode 100644 index 2f80a5cca..000000000 --- a/packages/create-plugin/templates/scenes-app/src/components/Routes/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './Routes'; diff --git a/packages/create-plugin/templates/scenes-app/src/module.ts b/packages/create-plugin/templates/scenes-app/src/module.ts deleted file mode 100644 index 319808b1b..000000000 --- a/packages/create-plugin/templates/scenes-app/src/module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AppPlugin } from '@grafana/data'; -import { App } from './components/App'; -import { AppConfig } from './components/AppConfig'; - -export const plugin = new AppPlugin<{}>().setRootPage(App).addConfigPage({ - title: 'Configuration', - icon: 'cog', - body: AppConfig, - id: 'configuration', -}); diff --git a/packages/create-plugin/templates/scenes-app/src/module.tsx b/packages/create-plugin/templates/scenes-app/src/module.tsx new file mode 100644 index 000000000..33fb72bd2 --- /dev/null +++ b/packages/create-plugin/templates/scenes-app/src/module.tsx @@ -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) => ( + }> + + +); + +const AppConfig = (props: AppConfigProps) => ( + }> + + +); + +export const plugin = new AppPlugin<{}>().setRootPage(App).addConfigPage({ + title: 'Configuration', + icon: 'cog', + body: AppConfig, + id: 'configuration', +}); diff --git a/packages/create-plugin/templates/scenes-app/src/pages/HelloWorld/HelloWorld.tsx b/packages/create-plugin/templates/scenes-app/src/pages/HelloWorld/HelloWorld.tsx index 8ef918630..27feac3dd 100644 --- a/packages/create-plugin/templates/scenes-app/src/pages/HelloWorld/HelloWorld.tsx +++ b/packages/create-plugin/templates/scenes-app/src/pages/HelloWorld/HelloWorld.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { getScene } from './helloWorldScene'; -export const HelloWorldPluginPage = () => { +const HelloWorld = () => { const scene = getScene(); return ; }; + +export default HelloWorld; diff --git a/packages/create-plugin/templates/scenes-app/src/pages/HelloWorld/index.tsx b/packages/create-plugin/templates/scenes-app/src/pages/HelloWorld/index.tsx deleted file mode 100644 index c7bdf54e1..000000000 --- a/packages/create-plugin/templates/scenes-app/src/pages/HelloWorld/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './HelloWorld'; diff --git a/packages/create-plugin/templates/scenes-app/src/pages/Home/Home.tsx b/packages/create-plugin/templates/scenes-app/src/pages/Home/Home.tsx index b5680b162..c00ce8950 100644 --- a/packages/create-plugin/templates/scenes-app/src/pages/Home/Home.tsx +++ b/packages/create-plugin/templates/scenes-app/src/pages/Home/Home.tsx @@ -22,7 +22,8 @@ const getScene = () => { ], }); }; -export const HomePage = () => { + +const HomePage = () => { const scene = useMemo(() => getScene(), []); return ( @@ -47,3 +48,5 @@ export const HomePage = () => { ); }; + +export default HomePage; diff --git a/packages/create-plugin/templates/scenes-app/src/pages/Home/index.tsx b/packages/create-plugin/templates/scenes-app/src/pages/Home/index.tsx deleted file mode 100644 index 6fd0b5ba7..000000000 --- a/packages/create-plugin/templates/scenes-app/src/pages/Home/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './Home'; diff --git a/packages/create-plugin/templates/scenes-app/src/pages/WithDrilldown/WithDrilldown.tsx b/packages/create-plugin/templates/scenes-app/src/pages/WithDrilldown/WithDrilldown.tsx index 594db1c3e..579faf84d 100644 --- a/packages/create-plugin/templates/scenes-app/src/pages/WithDrilldown/WithDrilldown.tsx +++ b/packages/create-plugin/templates/scenes-app/src/pages/WithDrilldown/WithDrilldown.tsx @@ -93,8 +93,10 @@ const getDrilldownsAppScene = () => { }); }; -export const WithDrilldown = () => { +const WithDrilldown = () => { const scene = useMemo(() => getDrilldownsAppScene(), []); return ; }; + +export default WithDrilldown; diff --git a/packages/create-plugin/templates/scenes-app/src/pages/WithDrilldown/index.tsx b/packages/create-plugin/templates/scenes-app/src/pages/WithDrilldown/index.tsx deleted file mode 100644 index 8c16fb721..000000000 --- a/packages/create-plugin/templates/scenes-app/src/pages/WithDrilldown/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './WithDrilldown'; diff --git a/packages/create-plugin/templates/scenes-app/src/pages/WithTabs/WithTabs.tsx b/packages/create-plugin/templates/scenes-app/src/pages/WithTabs/WithTabs.tsx index a1b06a72d..bdd36351c 100644 --- a/packages/create-plugin/templates/scenes-app/src/pages/WithTabs/WithTabs.tsx +++ b/packages/create-plugin/templates/scenes-app/src/pages/WithTabs/WithTabs.tsx @@ -38,8 +38,10 @@ const getScene = () => ], }); -export const PageWithTabs = () => { +const PageWithTabs = () => { const scene = useMemo(() => getScene(), []); return ; }; + +export default PageWithTabs; diff --git a/packages/create-plugin/templates/scenes-app/src/pages/WithTabs/index.tsx b/packages/create-plugin/templates/scenes-app/src/pages/WithTabs/index.tsx deleted file mode 100644 index ff1788ad5..000000000 --- a/packages/create-plugin/templates/scenes-app/src/pages/WithTabs/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './WithTabs';