mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
a2de893ab3
commit
1373b37166
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -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 +0,0 @@
|
||||
export * from './AppConfig';
|
@ -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 { App } from './components/App';
|
||||
import { testIds } from './testIds';
|
||||
|
||||
console.log('Hello from app B');
|
||||
import { testIds } from '../../testIds';
|
||||
|
||||
import { App } from './components/App';
|
||||
|
||||
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',
|
||||
},
|
||||
};
|
40
e2e/test-plugins/grafana-extensionstest-app/testIds.ts
Normal file
40
e2e/test-plugins/grafana-extensionstest-app/testIds.ts
Normal file
@ -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…
Reference in New Issue
Block a user