Plugin Extensions: E2E test addLink and legacy APIs (#92394)

* cleanup tests

* more cleanup

* added links

* test legacy hooks

* test legacy hooks

* update codeowners

* revert package changes

* add project specfic example script

* remove console log

* Update .github/CODEOWNERS

Co-authored-by: Timur Olzhabayev <timur.olzhabayev@grafana.com>

* Update CODEOWNERS

* use correct file names

* cleanup tests

---------

Co-authored-by: Timur Olzhabayev <timur.olzhabayev@grafana.com>
pull/91738/head^2
Erik Sundell 10 months ago committed by GitHub
parent a2de893ab3
commit 1373b37166
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      .github/CODEOWNERS
  2. 51
      e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts
  3. 44
      e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts
  4. 9
      e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts
  5. 11
      e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/usePluginComponents.spec.ts
  6. 2
      e2e/test-plugins/grafana-extensionstest-app/README.md
  7. 2
      e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx
  8. 12
      e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx
  9. 135
      e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/AppConfig.tsx
  10. 1
      e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/index.tsx
  11. 2
      e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx
  12. 36
      e2e/test-plugins/grafana-extensionstest-app/components/testIds.ts
  13. 4
      e2e/test-plugins/grafana-extensionstest-app/constants.ts
  14. 3
      e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx
  15. 25
      e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx
  16. 7
      e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx
  17. 44
      e2e/test-plugins/grafana-extensionstest-app/pages/LegacyAPIs.tsx
  18. 62
      e2e/test-plugins/grafana-extensionstest-app/pages/LegacyGetters.tsx
  19. 62
      e2e/test-plugins/grafana-extensionstest-app/pages/LegacyHooks.tsx
  20. 4
      e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx
  21. 20
      e2e/test-plugins/grafana-extensionstest-app/plugin.json
  22. 2
      e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx
  23. 15
      e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx
  24. 16
      e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/testIds.ts
  25. 5
      e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx
  26. 18
      e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/testIds.ts
  27. 40
      e2e/test-plugins/grafana-extensionstest-app/testIds.ts
  28. 52
      e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.getters.spec.ts
  29. 45
      e2e/test-plugins/grafana-extensionstest-app/tests/legacy/extensionPoints.hooks.spec.ts
  30. 42
      e2e/test-plugins/grafana-extensionstest-app/tests/legacy/linkExtensions.spec.ts
  31. 8
      e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts
  32. 11
      e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts
  33. 10
      e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts
  34. 0
      e2e/test-plugins/grafana-extensionstest-app/tests/utils.ts
  35. 9
      playwright.config.ts

@ -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

@ -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();
});

@ -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"');
});

@ -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!');
});

@ -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!');
});

@ -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`

@ -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';

@ -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 (
<div data-testid={testIds.container} style={{ marginTop: '5%' }}>
<Routes>
<Route path={ROUTES.LegacyAPIs} element={<LegacyAPIs />} />
<Route path={ROUTES.LegacyGetters} element={<LegacyGetters />} />
<Route path={ROUTES.LegacyHooks} element={<LegacyHooks />} />
<Route path={ROUTES.ExposedComponents} element={<ExposedComponents />} />
<Route path={ROUTES.AddedComponents} element={<AddedComponents />} />
<Route path={ROUTES.AddedLinks} element={<AddedLinks />} />
<Route path={'*'} element={<LegacyAPIs />} />
<Route path={'*'} element={<LegacyGetters />} />
</Routes>
</div>
);

@ -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<AppPluginMeta<AppPluginSettings>> {}
export const AppConfig = ({ plugin }: AppConfigProps) => {
const s = useStyles2(getStyles);
const { enabled, pinned, jsonData, secureJsonFields } = plugin.meta;
const [state, setState] = useState<State>({
apiUrl: jsonData?.apiUrl || '',
apiKey: '',
isApiKeySet: Boolean(secureJsonFields?.apiKey),
});
const onResetApiKey = () =>
setState({
...state,
apiKey: '',
isApiKeySet: false,
});
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setState({
...state,
[event.target.name]: event.target.value.trim(),
});
};
return (
<div data-testid={testIds.appConfig.container}>
<FieldSet label="API Settings">
<Field label="API Key" description="A secret key for authenticating to our custom API">
<SecretInput
width={60}
id="config-api-key"
data-testid={testIds.appConfig.apiKey}
name="apiKey"
value={state.apiKey}
isConfigured={state.isApiKeySet}
placeholder={'Your secret API key'}
onChange={onChange}
onReset={onResetApiKey}
/>
</Field>
<Field label="API Url" description="" className={s.marginTop}>
<Input
width={60}
name="apiUrl"
id="config-api-url"
data-testid={testIds.appConfig.apiUrl}
value={state.apiUrl}
placeholder={`E.g.: http://mywebsite.com/api/v1`}
onChange={onChange}
/>
</Field>
<div className={s.marginTop}>
<Button
type="submit"
data-testid={testIds.appConfig.submit}
onClick={() =>
updatePluginAndReload(plugin.meta.id, {
enabled,
pinned,
jsonData: {
apiUrl: state.apiUrl,
},
// This cannot be queried later by the frontend.
// We don't want to override it in case it was set previously and left untouched now.
secureJsonData: state.isApiKeySet
? undefined
: {
apiKey: state.apiKey,
},
})
}
disabled={Boolean(!state.apiUrl || (!state.isApiKeySet && !state.apiKey))}
>
Save API settings
</Button>
</div>
</FieldSet>
</div>
);
};
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<PluginMeta<AppPluginSettings>>) => {
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<PluginMeta>) => {
const response = await getBackendSrv().fetch({
url: `/api/plugins/${pluginId}/settings`,
method: 'POST',
data,
});
return lastValueFrom(response);
};

@ -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';

@ -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',
},
};

@ -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',
}

@ -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;
};

@ -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 (
<PluginPage>
<div data-testid={testIds.addedLinksPage.container}>
{isLoading ? (
<div>Loading...</div>
) : (
links.map(({ id, title, path, onClick }) => (
<a href={path} title={title} key={id} onClick={onClick}>
{title}
</a>
))
)}
</div>
</PluginPage>
);
}

@ -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<ReusableComponentProps>(
const { component: ReusableComponent } = usePluginComponent<ReusableComponentProps>(
'grafana-extensionexample1-app/reusable-component/v1'
);
@ -16,7 +17,7 @@ export function ExposedComponents() {
return (
<PluginPage>
<div data-testid={testIds.pageTwo.container}>
<div data-testid={testIds.exposedComponentsPage.container}>
<ReusableComponent name={'World'} />
</div>
</PluginPage>

@ -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<ReusableComponentProps>({
extensionPointId: 'plugins/grafana-extensionexample2-app/configure-extension-component/v1',
});
return (
<PluginPage>
<Stack direction={'column'} gap={4} data-testid={testIds.pageTwo.container}>
<article>
<h3>Link extensions defined with configureExtensionLink and retrived using getPluginExtensions</h3>
<ActionButton extensions={extensions} />
</article>
<article>
<h3>
Component extensions defined with configureExtensionComponent and retrived using
getPluginComponentExtensions
</h3>
{componentExtensions.map((extension) => {
const Component = extension.component;
return <Component key={extension.id} name="World" />;
})}
</article>
</Stack>
</PluginPage>
);
}

@ -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<ReusableComponentProps>({
extensionPointId: extensionPointId2,
});
return (
<PluginPage>
<Stack direction={'column'} gap={4} data-testid={testIds.legacyGettersPage.container}>
<section data-testid={testIds.legacyGettersPage.section1}>
<h3>
Link extensions defined with configureExtensionLink or configureExtensionComponent and retrived using
getPluginExtensions
</h3>
<ActionButton extensions={extensions} />
</section>
<section data-testid={testIds.legacyGettersPage.section2}>
<h3>Link extensions defined with configureExtensionLink and retrived using getPluginLinkExtensions</h3>
<ActionButton extensions={linkExtensions} />
</section>
<section data-testid={testIds.legacyGettersPage.section3}>
<h3>
Component extensions defined with configureExtensionComponent and retrived using
getPluginComponentExtensions
</h3>
{componentExtensions.map((extension) => {
const Component = extension.component;
return <Component key={extension.id} name="World" />;
})}
</section>
</Stack>
</PluginPage>
);
}

@ -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<ReusableComponentProps>({
extensionPointId: extensionPointId2,
});
return (
<PluginPage>
<Stack direction={'column'} gap={4} data-testid={testIds.legacyHooksPage.container}>
<section data-testid={testIds.legacyHooksPage.section1}>
<h3>
Link extensions defined with configureExtensionLink or configureExtensionComponent and retrived using
usePluginExtensions
</h3>
<ActionButton extensions={extensions} />
</section>
<section data-testid={testIds.legacyHooksPage.section2}>
<h3>Link extensions defined with configureExtensionLink and retrived using usePluginLinkExtensions</h3>
<ActionButton extensions={linkExtensions} />
</section>
<section data-testid={testIds.legacyHooksPage.section3}>
<h3>
Component extensions defined with configureExtensionComponent and retrived using
usePluginComponentExtensions
</h3>
{componentExtensions.map((extension) => {
const Component = extension.component;
return <Component key={extension.id} name="World" />;
})}
</section>
</Stack>
</PluginPage>
);
}

@ -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';

@ -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": {

@ -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<AppRootProps> {
render() {

@ -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 }) => <div data-testid="exposed-component">Hello {name}!</div>,
component: ({ name }: { name: string }) => <div data-testid={testIds.appB.exposedComponent}>Hello {name}!</div>,
})
.addLink({
title: 'Basic link',
description: '...',
targets: [LINKS_EXTENSION_POINT_ID],
path: `/a/${pluginJson.id}/`,
});

@ -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',
},
};

@ -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({

@ -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',
},
};

@ -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',
},
};

@ -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!');
});
});

@ -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!');
});
});

@ -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"');
});
});

@ -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!');
});

@ -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!');
});

@ -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!');
});

@ -70,5 +70,14 @@ export default defineConfig<PluginOptions>({
},
dependencies: ['authenticate'],
},
{
name: 'extensions-test-app',
testDir: 'e2e/test-plugins/grafana-extensionstest-app',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
],
});

Loading…
Cancel
Save