Plugin extensions: Add e2e tests (#89048)
* add custom plugins * update bundles * provision app plugins and their dashboards * add one more script that run e2e tests using e2e test server * add e2e tests * regenerate jsonnet dashboards * ignore custom plugins and playwright report * use minified * cleanup tests * update codeowners * add leading slash * document new script * document custom-plugins * cleanup * twist modules * add readme
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# App with extensions
|
||||
|
||||
This app was initially copied from the [app-with-extensions](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/app-with-extensions) example plugin. The plugin bundle is using AMD, but it's not minified and the plugin feature set is small so it should be possible to make changes in this file if necessary.
|
||||
|
||||
To test this app:
|
||||
|
||||
```sh
|
||||
# start e2e test instance (it will install this plugin)
|
||||
PORT=3000 ./scripts/grafana-server/start-server
|
||||
# run Playwright tests using Playwright VSCode extension or with the following script
|
||||
yarn e2e:playwright
|
||||
```
|
||||
@@ -0,0 +1,216 @@
|
||||
define(['react', '@grafana/data', '@grafana/ui', '@grafana/runtime', '@emotion/css', 'rxjs'], function (
|
||||
React,
|
||||
data,
|
||||
ui,
|
||||
runtime,
|
||||
css,
|
||||
rxjs
|
||||
) {
|
||||
'use strict';
|
||||
|
||||
const styles = {
|
||||
modalBody: 'ape-modal-body',
|
||||
mainPageContainer: 'ape-main-page-container',
|
||||
};
|
||||
|
||||
class RootComponent extends React.PureComponent {
|
||||
render() {
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ 'data-testid': styles.mainPageContainer, className: 'page-container' },
|
||||
'Hello Grafana!'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const asyncWrapper = (fn) => {
|
||||
return function () {
|
||||
const gen = fn.apply(this, arguments);
|
||||
return new Promise((resolve, reject) => {
|
||||
function step(key, arg) {
|
||||
let info, value;
|
||||
try {
|
||||
info = gen[key](arg);
|
||||
value = info.value;
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
if (info.done) {
|
||||
resolve(value);
|
||||
} else {
|
||||
Promise.resolve(value).then(next, throw_);
|
||||
}
|
||||
}
|
||||
function next(value) {
|
||||
step('next', value);
|
||||
}
|
||||
function throw_(value) {
|
||||
step('throw', value);
|
||||
}
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const getStyles = (theme) => ({
|
||||
colorWeak: css.css`color: ${theme.colors.text.secondary};`,
|
||||
marginTop: css.css`margin-top: ${theme.spacing(3)};`,
|
||||
});
|
||||
|
||||
const updatePlugin = asyncWrapper(function* (pluginId, settings) {
|
||||
const response = runtime
|
||||
.getBackendSrv()
|
||||
.fetch({ url: `/api/plugins/${pluginId}/settings`, method: 'POST', data: settings });
|
||||
return rxjs.lastValueFrom(response);
|
||||
});
|
||||
|
||||
const handleUpdate = asyncWrapper(function* (pluginId, settings) {
|
||||
try {
|
||||
yield updatePlugin(pluginId, settings);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('Error while updating the plugin', error);
|
||||
}
|
||||
});
|
||||
|
||||
const configPageBody = ({ plugin }) => {
|
||||
const styles = getStyles(ui.useStyles2());
|
||||
const { enabled, jsonData } = plugin.meta;
|
||||
return React.createElement(
|
||||
'div',
|
||||
null,
|
||||
React.createElement(ui.Legend, null, 'Enable / Disable '),
|
||||
!enabled &&
|
||||
React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
React.createElement('div', { className: styles.colorWeak }, 'The plugin is currently not enabled.'),
|
||||
React.createElement(
|
||||
ui.Button,
|
||||
{
|
||||
className: styles.marginTop,
|
||||
variant: 'primary',
|
||||
onClick: () => handleUpdate(plugin.meta.id, { enabled: true, pinned: true, jsonData: jsonData }),
|
||||
},
|
||||
'Enable plugin'
|
||||
)
|
||||
),
|
||||
enabled &&
|
||||
React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
React.createElement('div', { className: styles.colorWeak }, 'The plugin is currently enabled.'),
|
||||
React.createElement(
|
||||
ui.Button,
|
||||
{
|
||||
className: styles.marginTop,
|
||||
variant: 'destructive',
|
||||
onClick: () => handleUpdate(plugin.meta.id, { enabled: false, pinned: false, jsonData: jsonData }),
|
||||
},
|
||||
'Disable plugin'
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const selectQueryModal = ({ targets = [], onDismiss }) => {
|
||||
const [selectedQuery, setSelectedQuery] = React.useState(targets[0]);
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ 'data-testid': styles.modalBody },
|
||||
React.createElement(
|
||||
'p',
|
||||
null,
|
||||
'Please select the query you would like to use to create "something" in the plugin.'
|
||||
),
|
||||
React.createElement(
|
||||
ui.HorizontalGroup,
|
||||
null,
|
||||
targets.map((query) =>
|
||||
React.createElement(ui.FilterPill, {
|
||||
key: query.refId,
|
||||
label: query.refId,
|
||||
selected: query.refId === (selectedQuery ? selectedQuery.refId : null),
|
||||
onClick: () => setSelectedQuery(query),
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
ui.Modal.ButtonRow,
|
||||
null,
|
||||
React.createElement(ui.Button, { variant: 'secondary', fill: 'outline', onClick: onDismiss }, 'Cancel'),
|
||||
React.createElement(
|
||||
ui.Button,
|
||||
{
|
||||
disabled: !Boolean(selectedQuery),
|
||||
onClick: () => {
|
||||
onDismiss && onDismiss();
|
||||
alert(`You selected query "${selectedQuery.refId}"`);
|
||||
},
|
||||
},
|
||||
'OK'
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const plugin = new data.AppPlugin()
|
||||
.setRootPage(RootComponent)
|
||||
.addConfigPage({
|
||||
title: 'Configuration',
|
||||
icon: 'cog',
|
||||
body: configPageBody,
|
||||
id: 'configuration',
|
||||
})
|
||||
.configureExtensionLink({
|
||||
title: 'Open from time series or pie charts (path)',
|
||||
description: 'This link will only be visible on time series and pie charts',
|
||||
extensionPointId: data.PluginExtensionPoints.DashboardPanelMenu,
|
||||
path: `/a/myorg-extensions-app/`,
|
||||
configure: (context) => {
|
||||
if (context.dashboard?.title === 'Link Extensions (path)') {
|
||||
switch (context.pluginId) {
|
||||
case 'timeseries':
|
||||
return {};
|
||||
case 'piechart':
|
||||
return { title: `Open from ${context.pluginId}` };
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
.configureExtensionLink({
|
||||
title: 'Open from time series or pie charts (onClick)',
|
||||
description: 'This link will only be visible on time series and pie charts',
|
||||
extensionPointId: data.PluginExtensionPoints.DashboardPanelMenu,
|
||||
onClick: (_, { context, openModal }) => {
|
||||
const targets = context?.targets || [];
|
||||
const title = context?.title;
|
||||
if (!targets.length) return;
|
||||
if (targets.length > 1) {
|
||||
openModal({
|
||||
title: `Select query from "${title}"`,
|
||||
body: (props) => React.createElement(selectQueryModal, { ...props, targets: targets }),
|
||||
});
|
||||
} else {
|
||||
alert(`You selected query "${targets[0].refId}"`);
|
||||
}
|
||||
},
|
||||
configure: (context) => {
|
||||
if (context.dashboard?.title === 'Link Extensions (onClick)') {
|
||||
switch (context.pluginId) {
|
||||
case 'timeseries':
|
||||
return {};
|
||||
case 'piechart':
|
||||
return { title: `Open from ${context.pluginId}` };
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return { plugin: plugin };
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
|
||||
"type": "app",
|
||||
"name": "Extensions App",
|
||||
"id": "myorg-extensions-app",
|
||||
"preload": true,
|
||||
"info": {
|
||||
"keywords": ["app"],
|
||||
"description": "Example on how to extend grafana ui from a plugin",
|
||||
"author": {
|
||||
"name": "Myorg"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/logo.svg",
|
||||
"large": "img/logo.svg"
|
||||
},
|
||||
"screenshots": [],
|
||||
"version": "1.0.0",
|
||||
"updated": "2024-06-11"
|
||||
},
|
||||
"includes": [
|
||||
{
|
||||
"type": "page",
|
||||
"name": "Default",
|
||||
"path": "/a/myorg-extensions-app",
|
||||
"role": "Admin",
|
||||
"addToNav": true,
|
||||
"defaultNav": true
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=10.3.3",
|
||||
"plugins": []
|
||||
},
|
||||
"extensions": [
|
||||
{
|
||||
"extensionPointId": "grafana/dashboard/panel/menu",
|
||||
"type": "link",
|
||||
"title": "Open from time series or pie charts (path)",
|
||||
"description": "This link will only be visible on time series and pie charts"
|
||||
},
|
||||
{
|
||||
"extensionPointId": "grafana/dashboard/panel/menu",
|
||||
"type": "link",
|
||||
"title": "Open from time series or pie charts (onClick)",
|
||||
"description": "This link will only be visible on time series and pie charts"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user