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'],
+ },
],
});