diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c92f3f3d701..82f950b572c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -319,6 +319,7 @@ /e2e/ @grafana/grafana-frontend-platform /e2e/cloud-plugins-suite/ @grafana/partner-datasources /e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend +/e2e/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend # Packages /packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts deleted file mode 100644 index 902adc04adf..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -import { ensureExtensionRegistryIsPopulated } from './utils'; - -const testIds = { - container: 'main-app-body', - actions: { - button: 'action-button', - }, - modal: { - container: 'container', - open: 'open-link', - }, - appA: { - container: 'a-app-body', - }, - appB: { - modal: 'b-app-modal', - reusableComponent: 'b-app-configure-extension-component', - }, - legacyAPIPage: { - container: 'data-testid pg-two-container', - }, -}; - -const pluginId = 'grafana-extensionstest-app'; - -test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { - await page.goto(`/a/${pluginId}/legacy-apis`); - await ensureExtensionRegistryIsPopulated(page); - await page.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Go to A').click(); - await page.getByTestId(testIds.modal.open).click(); - await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); -}); - -test('should extend the actions menu with a command triggered from b-app plugin', async ({ page }) => { - await page.goto(`/a/${pluginId}/legacy-apis`); - await ensureExtensionRegistryIsPopulated(page); - await expect( - page.getByTestId(testIds.legacyAPIPage.container).getByTestId(testIds.appB.reusableComponent) - ).toHaveText('Hello World!'); -}); - -test('should extend main app with component extension from app B', async ({ page }) => { - await page.goto(`/a/${pluginId}/legacy-apis`); - await ensureExtensionRegistryIsPopulated(page); - await page.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Open from B').click(); - await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts deleted file mode 100644 index b87b4eb0bca..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { selectors } from '@grafana/e2e-selectors'; -import { expect, test } from '@grafana/plugin-e2e'; - -import { ensureExtensionRegistryIsPopulated } from './utils'; - -const panelTitle = 'Link with defaults'; -const extensionTitle = 'Open from time series...'; -const testIds = { - modal: { - container: 'ape-modal-body', - }, - mainPage: { - container: 'main-app-body', - }, -}; - -const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946'; -const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719'; - -test('should add link extension (path) with defaults to time series panel', async ({ gotoDashboardPage, page }) => { - const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid }); - await ensureExtensionRegistryIsPopulated(page); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByTestId(testIds.mainPage.container)).toBeVisible(); -}); - -test('should add link extension (onclick) with defaults to time series panel', async ({ gotoDashboardPage, page }) => { - const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); - await ensureExtensionRegistryIsPopulated(page); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"'); -}); - -test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => { - const panelTitle = 'Link with new name'; - const extensionTitle = 'Open from piechart'; - const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); - await ensureExtensionRegistryIsPopulated(page); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts deleted file mode 100644 index 20676945698..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -const pluginId = 'grafana-extensionstest-app'; -const exposedComponentTestId = 'exposed-component'; - -test('should display component exposed by another app', async ({ page }) => { - await page.goto(`/a/${pluginId}/exposed-components`); - await expect(await page.getByTestId(exposedComponentTestId)).toHaveText('Hello World!'); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/usePluginComponents.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/usePluginComponents.spec.ts deleted file mode 100644 index 0b1be323949..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/usePluginComponents.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -const pluginId = 'grafana-extensionstest-app'; -const exposedComponentTestId = 'exposed-component'; - -test('should render component with usePluginComponents hook', async ({ page }) => { - await page.goto(`/a/${pluginId}/added-components`); - await expect( - page.getByTestId('data-testid pg-added-components-container').getByTestId('b-app-add-component') - ).toHaveText('Hello World!'); -}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/README.md b/e2e/test-plugins/grafana-extensionstest-app/README.md index 02b4f9d4690..e92e2b3f064 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/README.md +++ b/e2e/test-plugins/grafana-extensionstest-app/README.md @@ -32,4 +32,4 @@ Note that this plugin extends the `@grafana/plugin-configs` configs which is why ## Run Playwright tests -- `yarn e2e:playwright` +- `yarn playwright --project extensions-test-app` diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx index 62758171301..565917345a3 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx +++ b/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx @@ -1,7 +1,7 @@ import { PluginExtension, PluginExtensionLink, SelectableValue, locationUtil } from '@grafana/data'; import { isPluginExtensionLink, locationService } from '@grafana/runtime'; import { Button, ButtonGroup, ButtonSelect, Modal, Stack, ToolbarButton } from '@grafana/ui'; -import { testIds } from '../testIds'; +import { testIds } from '../../testIds'; import { ReactElement, useMemo, useState } from 'react'; diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx index 23dc821f4be..2a413c665a6 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx +++ b/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx @@ -1,18 +1,22 @@ import { Route, Routes } from 'react-router-dom'; + import { AppRootProps } from '@grafana/data'; + import { ROUTES } from '../../constants'; -import { AddedComponents, ExposedComponents, LegacyAPIs } from '../../pages'; -import { testIds } from '../testIds'; +import { AddedComponents, AddedLinks, ExposedComponents, LegacyGetters, LegacyHooks } from '../../pages'; +import { testIds } from '../../testIds'; export function App(props: AppRootProps) { return (
- } /> + } /> + } /> } /> } /> + } /> - } /> + } />
); diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/AppConfig.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/AppConfig.tsx deleted file mode 100644 index 44e2d4415c0..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/AppConfig.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { ChangeEvent, useState } from 'react'; -import { lastValueFrom } from 'rxjs'; -import { css } from '@emotion/css'; -import { AppPluginMeta, GrafanaTheme2, PluginConfigPageProps, PluginMeta } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; -import { Button, Field, FieldSet, Input, SecretInput, useStyles2 } from '@grafana/ui'; -import { testIds } from '../testIds'; - -export type AppPluginSettings = { - apiUrl?: string; -}; - -type State = { - // The URL to reach our custom API. - apiUrl: string; - // Tells us if the API key secret is set. - isApiKeySet: boolean; - // A secret key for our custom API. - apiKey: string; -}; - -export interface AppConfigProps extends PluginConfigPageProps> {} - -export const AppConfig = ({ plugin }: AppConfigProps) => { - const s = useStyles2(getStyles); - const { enabled, pinned, jsonData, secureJsonFields } = plugin.meta; - const [state, setState] = useState({ - apiUrl: jsonData?.apiUrl || '', - apiKey: '', - isApiKeySet: Boolean(secureJsonFields?.apiKey), - }); - - const onResetApiKey = () => - setState({ - ...state, - apiKey: '', - isApiKeySet: false, - }); - - const onChange = (event: ChangeEvent) => { - setState({ - ...state, - [event.target.name]: event.target.value.trim(), - }); - }; - - return ( -
-
- - - - - - - - -
- -
-
-
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - colorWeak: css` - color: ${theme.colors.text.secondary}; - `, - marginTop: css` - margin-top: ${theme.spacing(3)}; - `, -}); - -const updatePluginAndReload = async (pluginId: string, data: Partial>) => { - try { - await updatePlugin(pluginId, data); - - // Reloading the page as the changes made here wouldn't be propagated to the actual plugin otherwise. - // This is not ideal, however unfortunately currently there is no supported way for updating the plugin state. - window.location.reload(); - } catch (e) { - console.error('Error while updating the plugin', e); - } -}; - -export const updatePlugin = async (pluginId: string, data: Partial) => { - const response = await getBackendSrv().fetch({ - url: `/api/plugins/${pluginId}/settings`, - method: 'POST', - data, - }); - - return lastValueFrom(response); -}; diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/index.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/index.tsx deleted file mode 100644 index 1dba18f08fd..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './AppConfig'; diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx index f31e3b131b3..bf6d8f43015 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx +++ b/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx @@ -1,6 +1,6 @@ import { DataQuery } from '@grafana/data'; import { Button, FilterPill, Modal, Stack } from '@grafana/ui'; -import { testIds } from '../testIds'; +import { testIds } from '../../testIds'; import { ReactElement, useState } from 'react'; import { selectQuery } from '../../utils/utils'; diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/testIds.ts b/e2e/test-plugins/grafana-extensionstest-app/components/testIds.ts deleted file mode 100644 index 409baf608c9..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/components/testIds.ts +++ /dev/null @@ -1,36 +0,0 @@ -export const testIds = { - container: 'main-app-body', - actions: { - button: 'action-button', - }, - modal: { - container: 'container', - open: 'open-link', - }, - appA: { - container: 'a-app-body', - }, - appB: { - modal: 'b-app-modal', - }, - appConfig: { - container: 'data-testid ac-container', - apiKey: 'data-testid ac-api-key', - apiUrl: 'data-testid ac-api-url', - submit: 'data-testid ac-submit-form', - }, - pageOne: { - container: 'data-testid pg-one-container', - navigateToFour: 'data-testid navigate-to-four', - }, - pageTwo: { - container: 'data-testid pg-two-container', - }, - addedComponentsPage: { - container: 'data-testid pg-added-components-container', - }, - pageFour: { - container: 'data-testid pg-four-container', - navigateBack: 'data-testid navigate-back', - }, -}; diff --git a/e2e/test-plugins/grafana-extensionstest-app/constants.ts b/e2e/test-plugins/grafana-extensionstest-app/constants.ts index e259887ca99..120eb5d8191 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/constants.ts +++ b/e2e/test-plugins/grafana-extensionstest-app/constants.ts @@ -3,7 +3,9 @@ import pluginJson from './plugin.json'; export const PLUGIN_BASE_URL = `/a/${pluginJson.id}`; export enum ROUTES { - LegacyAPIs = 'legacy-apis', + LegacyGetters = 'legacy-getters', + LegacyHooks = 'legacy-hooks', ExposedComponents = 'exposed-components', AddedComponents = 'added-components', + AddedLinks = 'added-links', } diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx index e15dc0a3482..b7f3242f3a6 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx +++ b/e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx @@ -1,7 +1,8 @@ -import { testIds } from '../components/testIds'; import { PluginPage, usePluginComponents } from '@grafana/runtime'; import { Stack } from '@grafana/ui'; +import { testIds } from '../testIds'; + type ReusableComponentProps = { name: string; }; diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx new file mode 100644 index 00000000000..dbfd00c36c4 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx @@ -0,0 +1,25 @@ +import { PluginPage, usePluginLinks } from '@grafana/runtime'; + +import { testIds } from '../testIds'; + +export const LINKS_EXTENSION_POINT_ID = 'plugins/grafana-extensionstest-app/use-plugin-links/v1'; + +export function AddedLinks() { + const { links, isLoading } = usePluginLinks({ extensionPointId: LINKS_EXTENSION_POINT_ID }); + + return ( + +
+ {isLoading ? ( +
Loading...
+ ) : ( + links.map(({ id, title, path, onClick }) => ( + + {title} + + )) + )} +
+
+ ); +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx index d43b84a2da9..28775391881 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx +++ b/e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx @@ -1,12 +1,13 @@ -import { testIds } from '../components/testIds'; import { PluginPage, usePluginComponent } from '@grafana/runtime'; +import { testIds } from '../testIds'; + type ReusableComponentProps = { name: string; }; export function ExposedComponents() { - var { component: ReusableComponent } = usePluginComponent( + const { component: ReusableComponent } = usePluginComponent( 'grafana-extensionexample1-app/reusable-component/v1' ); @@ -16,7 +17,7 @@ export function ExposedComponents() { return ( -
+
diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyAPIs.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyAPIs.tsx deleted file mode 100644 index 9407a2fca3f..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyAPIs.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { testIds } from '../components/testIds'; -import { PluginPage, getPluginComponentExtensions, getPluginExtensions } from '@grafana/runtime'; -import { ActionButton } from '../components/ActionButton'; -import { Stack } from '@grafana/ui'; - -type AppExtensionContext = {}; -type ReusableComponentProps = { - name: string; -}; - -export function LegacyAPIs() { - const extensionPointId = 'plugins/grafana-extensionstest-app/actions'; - const context: AppExtensionContext = {}; - - const { extensions } = getPluginExtensions({ - extensionPointId, - context, - }); - - const { extensions: componentExtensions } = getPluginComponentExtensions({ - extensionPointId: 'plugins/grafana-extensionexample2-app/configure-extension-component/v1', - }); - - return ( - - -
-

Link extensions defined with configureExtensionLink and retrived using getPluginExtensions

- -
-
-

- Component extensions defined with configureExtensionComponent and retrived using - getPluginComponentExtensions -

- {componentExtensions.map((extension) => { - const Component = extension.component; - return ; - })} -
-
-
- ); -} diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyGetters.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyGetters.tsx new file mode 100644 index 00000000000..910eed92e5b --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyGetters.tsx @@ -0,0 +1,62 @@ +import { + PluginPage, + getPluginComponentExtensions, + getPluginExtensions, + getPluginLinkExtensions, +} from '@grafana/runtime'; +import { Stack } from '@grafana/ui'; + +import { ActionButton } from '../components/ActionButton'; +import { testIds } from '../testIds'; + +type AppExtensionContext = {}; +type ReusableComponentProps = { + name: string; +}; + +export function LegacyGetters() { + const extensionPointId1 = 'plugins/grafana-extensionstest-app/actions'; + const extensionPointId2 = 'plugins/grafana-extensionexample2-app/configure-extension-component/v1'; + const context: AppExtensionContext = {}; + + const { extensions } = getPluginExtensions({ + extensionPointId: extensionPointId1, + context, + }); + + const { extensions: linkExtensions } = getPluginLinkExtensions({ + extensionPointId: extensionPointId1, + }); + + const { extensions: componentExtensions } = getPluginComponentExtensions({ + extensionPointId: extensionPointId2, + }); + + return ( + + +
+

+ Link extensions defined with configureExtensionLink or configureExtensionComponent and retrived using + getPluginExtensions +

+ +
+
+

Link extensions defined with configureExtensionLink and retrived using getPluginLinkExtensions

+ +
+
+

+ Component extensions defined with configureExtensionComponent and retrived using + getPluginComponentExtensions +

+ {componentExtensions.map((extension) => { + const Component = extension.component; + return ; + })} +
+
+
+ ); +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyHooks.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyHooks.tsx new file mode 100644 index 00000000000..ab963f07169 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyHooks.tsx @@ -0,0 +1,62 @@ +import { + PluginPage, + usePluginComponentExtensions, + usePluginExtensions, + usePluginLinkExtensions, +} from '@grafana/runtime'; +import { Stack } from '@grafana/ui'; + +import { ActionButton } from '../components/ActionButton'; +import { testIds } from '../testIds'; + +type AppExtensionContext = {}; +type ReusableComponentProps = { + name: string; +}; + +export function LegacyHooks() { + const extensionPointId1 = 'plugins/grafana-extensionstest-app/actions'; + const extensionPointId2 = 'plugins/grafana-extensionexample2-app/configure-extension-component/v1'; + const context: AppExtensionContext = {}; + + const { extensions } = usePluginExtensions({ + extensionPointId: extensionPointId1, + context, + }); + + const { extensions: linkExtensions } = usePluginLinkExtensions({ + extensionPointId: extensionPointId1, + }); + + const { extensions: componentExtensions } = usePluginComponentExtensions({ + extensionPointId: extensionPointId2, + }); + + return ( + + +
+

+ Link extensions defined with configureExtensionLink or configureExtensionComponent and retrived using + usePluginExtensions +

+ +
+
+

Link extensions defined with configureExtensionLink and retrived using usePluginLinkExtensions

+ +
+
+

+ Component extensions defined with configureExtensionComponent and retrived using + usePluginComponentExtensions +

+ {componentExtensions.map((extension) => { + const Component = extension.component; + return ; + })} +
+
+
+ ); +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx index 2afacedc3be..6e955da302b 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx +++ b/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx @@ -1,3 +1,5 @@ export { ExposedComponents } from './ExposedComponents'; -export { LegacyAPIs } from './LegacyAPIs'; +export { LegacyGetters } from './LegacyGetters'; +export { LegacyHooks } from './LegacyHooks'; export { AddedComponents } from './AddedComponents'; +export { AddedLinks } from './AddedLinks'; diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugin.json b/e2e/test-plugins/grafana-extensionstest-app/plugin.json index aa6bae38085..0924f5aad10 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/plugin.json +++ b/e2e/test-plugins/grafana-extensionstest-app/plugin.json @@ -21,8 +21,16 @@ "includes": [ { "type": "page", - "name": "Legacy APIs", - "path": "/a/grafana-extensionstest-app/legacy-apis", + "name": "Legacy Getters", + "path": "/a/grafana-extensionstest-app/legacy-getters", + "role": "Admin", + "addToNav": true, + "defaultNav": false + }, + { + "type": "page", + "name": "Legacy Hooks", + "path": "/a/grafana-extensionstest-app/legacy-hooks", "role": "Admin", "addToNav": true, "defaultNav": false @@ -45,11 +53,11 @@ }, { "type": "page", - "icon": "cog", - "name": "Configuration", - "path": "/plugins/grafana-extensionstest-app", + "name": "Added links", + "path": "/a/grafana-extensionstest-app/added-links", "role": "Admin", - "addToNav": true + "addToNav": true, + "defaultNav": false } ], "dependencies": { diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx index b5cbe8713b7..cb3af35cd37 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppRootProps } from '@grafana/data'; -import { testIds } from '../../testIds'; +import { testIds } from '../../../../testIds'; export class App extends React.PureComponent { render() { diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx index 602ad153afb..ae1a2efe4d9 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx @@ -1,5 +1,10 @@ import { AppPlugin } from '@grafana/data'; + +import { LINKS_EXTENSION_POINT_ID } from '../../pages/AddedLinks'; +import { testIds } from '../../testIds'; + import { App } from './components/App'; +import pluginJson from './plugin.json'; export const plugin = new AppPlugin<{}>() .setRootPage(App) @@ -11,7 +16,13 @@ export const plugin = new AppPlugin<{}>() }) .exposeComponent({ id: 'grafana-extensionexample1-app/reusable-component/v1', - title: 'Reusable component', + title: 'Exposed component', description: 'A component that can be reused by other app plugins.', - component: ({ name }: { name: string }) =>
Hello {name}!
, + component: ({ name }: { name: string }) =>
Hello {name}!
, + }) + .addLink({ + title: 'Basic link', + description: '...', + targets: [LINKS_EXTENSION_POINT_ID], + path: `/a/${pluginJson.id}/`, }); diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/testIds.ts b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/testIds.ts deleted file mode 100644 index 5bce9218a25..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/testIds.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const testIds = { - container: 'main-app-body', - actions: { - button: 'action-button', - }, - modal: { - container: 'container', - open: 'open-link', - }, - appA: { - container: 'a-app-body', - }, - appB: { - modal: 'b-app-modal', - }, -}; diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx index 6a5e0d3a0b9..7c68c104597 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx @@ -1,8 +1,9 @@ import { AppPlugin } from '@grafana/data'; + +import { testIds } from '../../testIds'; + import { App } from './components/App'; -import { testIds } from './testIds'; -console.log('Hello from app B'); export const plugin = new AppPlugin<{}>() .setRootPage(App) .configureExtensionLink({ diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/testIds.ts b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/testIds.ts deleted file mode 100644 index 240a7710d7a..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/testIds.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const testIds = { - container: 'main-app-body', - actions: { - button: 'action-button', - }, - modal: { - container: 'container', - open: 'open-link', - }, - appA: { - container: 'a-app-body', - }, - appB: { - modal: 'b-app-modal', - reusableComponent: 'b-app-configure-extension-component', - reusableAddedComponent: 'b-app-add-component', - }, -}; diff --git a/e2e/test-plugins/grafana-extensionstest-app/testIds.ts b/e2e/test-plugins/grafana-extensionstest-app/testIds.ts new file mode 100644 index 00000000000..29a9c0b0726 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/testIds.ts @@ -0,0 +1,40 @@ +export const testIds = { + container: 'main-app-body', + actions: { + button: 'action-button', + }, + modal: { + container: 'container', + open: 'open-link', + }, + appA: { + container: 'a-app-body', + }, + appB: { + modal: 'b-app-modal', + reusableComponent: 'b-app-configure-extension-component', + reusableAddedComponent: 'b-app-add-component', + exposedComponent: 'b-app-exposed-component', + }, + legacyGettersPage: { + container: 'data-testid pg-legacy-getters-container', + section1: 'get-plugin-extensions', + section2: 'configure-extension-link-get-plugin-link-extensions', + section3: 'configure-extension-component-get-plugin-component-extensions', + }, + legacyHooksPage: { + container: 'data-testid pg-legacy-hooks-container', + section1: 'use-plugin-extensions', + section2: 'configure-extension-link-use-plugin-link-extensions', + section3: 'configure-extension-component-use-plugin-component-extensions', + }, + exposedComponentsPage: { + container: 'data-testid pg-exposed-components-container', + }, + addedComponentsPage: { + container: 'data-testid pg-added-components-container', + }, + addedLinksPage: { + container: 'data-testid pg-added-links-container', + }, +}; diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.getters.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.getters.spec.ts new file mode 100644 index 00000000000..0d451b56977 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.getters.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { ensureExtensionRegistryIsPopulated } from '../utils'; +import { testIds } from '../../testIds'; +import pluginJson from '../../plugin.json'; + +test.describe('getPluginExtensions + configureExtensionLink', () => { + test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/legacy-getters`); + await ensureExtensionRegistryIsPopulated(page); + const section = await page.getByTestId(testIds.legacyGettersPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Go to A').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); + }); +}); + +test.describe('getPluginExtensions + configureExtensionComponent', () => { + test('should extend main app with component extension from app B', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/legacy-getters`); + await ensureExtensionRegistryIsPopulated(page); + const section = await page.getByTestId(testIds.legacyGettersPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Open from B').click(); + await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); + }); +}); + +test.describe('getPluginLinkExtensions + configureExtensionLink', () => { + test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/legacy-getters`); + await ensureExtensionRegistryIsPopulated(page); + const section = await page.getByTestId(testIds.legacyGettersPage.section2); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Go to A').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); + }); +}); + +test.describe('getPluginComponentExtensions + configureExtensionComponent', () => { + test('should extend the actions menu with a command triggered from b-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/legacy-getters`); + await ensureExtensionRegistryIsPopulated(page); + await expect( + page + .getByTestId('configure-extension-component-get-plugin-component-extensions') + .getByTestId(testIds.appB.reusableComponent) + ).toHaveText('Hello World!'); + }); +}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.hooks.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.hooks.spec.ts new file mode 100644 index 00000000000..d5c96eefa31 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.hooks.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { testIds } from '../../testIds'; +import pluginJson from '../../plugin.json'; + +test.describe('usePluginExtensions + configureExtensionLink', () => { + test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/legacy-hooks`); + const section = await page.getByTestId(testIds.legacyHooksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Go to A').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); + }); +}); + +test.describe('usePluginExtensions + configureExtensionComponent', () => { + test('should extend main app with component extension from app B', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/legacy-hooks`); + const section = await page.getByTestId(testIds.legacyHooksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Open from B').click(); + await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); + }); +}); + +test.describe('usePluginLinkExtensions + configureExtensionLink', () => { + test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/legacy-hooks`); + const section = await page.getByTestId(testIds.legacyHooksPage.section2); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Go to A').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); + }); +}); + +test.describe('usePluginComponentExtensions + configureExtensionComponent', () => { + test('should extend the actions menu with a command triggered from b-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/legacy-hooks`); + await expect( + page.getByTestId(testIds.legacyHooksPage.section3).getByTestId(testIds.appB.reusableComponent) + ).toHaveText('Hello World!'); + }); +}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/linkExtensions.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/linkExtensions.spec.ts new file mode 100644 index 00000000000..20be9c23d3f --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/tests/legacy/linkExtensions.spec.ts @@ -0,0 +1,42 @@ +import { expect, test } from '@grafana/plugin-e2e'; +import { ensureExtensionRegistryIsPopulated } from '../utils'; + +const panelTitle = 'Link with defaults'; +const extensionTitle = 'Open from time series...'; + +const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946'; +const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719'; + +test.describe('configureExtensionLink targeting core extension points', () => { + test('configureExtensionLink - should add link extension (path) with defaults to time series panel.', async ({ + gotoDashboardPage, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid }); + await ensureExtensionRegistryIsPopulated(page); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('heading', { name: 'Extensions test app' })).toBeVisible(); + }); + + test('should add link extension (onclick) with defaults to time series panel', async ({ + gotoDashboardPage, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + await ensureExtensionRegistryIsPopulated(page); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"'); + }); + + test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => { + const panelTitle = 'Link with new name'; + const extensionTitle = 'Open from piechart'; + const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + await ensureExtensionRegistryIsPopulated(page); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); + }); +}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts new file mode 100644 index 00000000000..12d270ae565 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts @@ -0,0 +1,8 @@ +import { test, expect } from '@grafana/plugin-e2e'; +import { testIds } from '../testIds'; +import pluginJson from '../plugin.json'; + +test('should display component exposed by another app', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/exposed-components`); + await expect(page.getByTestId(testIds.appB.exposedComponent)).toHaveText('Hello World!'); +}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts new file mode 100644 index 00000000000..a0d6d2246a0 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import pluginJson from '../plugin.json'; +import { testIds } from '../testIds'; + +test('should render component with usePluginComponents hook', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-components`); + await expect( + page.getByTestId(testIds.addedComponentsPage.container).getByTestId(testIds.appB.reusableAddedComponent) + ).toHaveText('Hello World!'); +}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts new file mode 100644 index 00000000000..cfeef4bfcdf --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts @@ -0,0 +1,10 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import pluginJson from '../plugin.json'; +import { testIds } from '../testIds'; + +test('path link', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-links`); + await page.getByTestId(testIds.addedLinksPage.container).getByText('Basic link').click(); + await expect(page.getByTestId(testIds.appA.container)).toHaveText('Hello Grafana!'); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/utils.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/utils.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/utils.ts rename to e2e/test-plugins/grafana-extensionstest-app/tests/utils.ts diff --git a/playwright.config.ts b/playwright.config.ts index a4abcbd0263..3dc0984aef1 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -70,5 +70,14 @@ export default defineConfig({ }, dependencies: ['authenticate'], }, + { + name: 'extensions-test-app', + testDir: 'e2e/test-plugins/grafana-extensionstest-app', + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, ], });