mirror of https://github.com/grafana/grafana
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 readmepull/89220/head
parent
a9171aa9fe
commit
72241dbf5f
@ -0,0 +1,342 @@ |
||||
{ |
||||
"annotations": { |
||||
"list": [ |
||||
{ |
||||
"builtIn": 1, |
||||
"datasource": { |
||||
"type": "grafana", |
||||
"uid": "-- Grafana --" |
||||
}, |
||||
"enable": true, |
||||
"hide": true, |
||||
"iconColor": "rgba(0, 211, 255, 1)", |
||||
"name": "Annotations & Alerts", |
||||
"type": "dashboard" |
||||
} |
||||
] |
||||
}, |
||||
"editable": true, |
||||
"fiscalYearStartMonth": 0, |
||||
"graphTooltip": 0, |
||||
"links": [], |
||||
"liveNow": false, |
||||
"panels": [ |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"fieldConfig": { |
||||
"defaults": { |
||||
"color": { |
||||
"mode": "palette-classic" |
||||
}, |
||||
"custom": { |
||||
"axisCenteredZero": false, |
||||
"axisColorMode": "text", |
||||
"axisLabel": "", |
||||
"axisPlacement": "auto", |
||||
"barAlignment": 0, |
||||
"drawStyle": "line", |
||||
"fillOpacity": 0, |
||||
"gradientMode": "none", |
||||
"hideFrom": { |
||||
"legend": false, |
||||
"tooltip": false, |
||||
"viz": false |
||||
}, |
||||
"lineInterpolation": "linear", |
||||
"lineWidth": 1, |
||||
"pointSize": 5, |
||||
"scaleDistribution": { |
||||
"type": "linear" |
||||
}, |
||||
"showPoints": "auto", |
||||
"spanNulls": false, |
||||
"stacking": { |
||||
"group": "A", |
||||
"mode": "none" |
||||
}, |
||||
"thresholdsStyle": { |
||||
"mode": "off" |
||||
} |
||||
}, |
||||
"mappings": [], |
||||
"thresholds": { |
||||
"mode": "absolute", |
||||
"steps": [ |
||||
{ |
||||
"color": "green", |
||||
"value": null |
||||
}, |
||||
{ |
||||
"color": "red", |
||||
"value": 80 |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"overrides": [] |
||||
}, |
||||
"gridPos": { |
||||
"h": 8, |
||||
"w": 12, |
||||
"x": 0, |
||||
"y": 0 |
||||
}, |
||||
"id": 7, |
||||
"options": { |
||||
"legend": { |
||||
"calcs": [], |
||||
"displayMode": "list", |
||||
"placement": "bottom", |
||||
"showLegend": true |
||||
}, |
||||
"tooltip": { |
||||
"mode": "single", |
||||
"sort": "none" |
||||
} |
||||
}, |
||||
"title": "Link with one query", |
||||
"type": "timeseries" |
||||
}, |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"fieldConfig": { |
||||
"defaults": { |
||||
"color": { |
||||
"mode": "thresholds" |
||||
}, |
||||
"custom": { |
||||
"align": "auto", |
||||
"cellOptions": { |
||||
"type": "auto" |
||||
}, |
||||
"inspect": false |
||||
}, |
||||
"mappings": [], |
||||
"thresholds": { |
||||
"mode": "absolute", |
||||
"steps": [ |
||||
{ |
||||
"color": "green", |
||||
"value": null |
||||
}, |
||||
{ |
||||
"color": "red", |
||||
"value": 80 |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"overrides": [] |
||||
}, |
||||
"gridPos": { |
||||
"h": 8, |
||||
"w": 6, |
||||
"x": 0, |
||||
"y": 8 |
||||
}, |
||||
"id": 6, |
||||
"options": { |
||||
"cellHeight": "sm", |
||||
"footer": { |
||||
"countRows": false, |
||||
"fields": "", |
||||
"reducer": [ |
||||
"sum" |
||||
], |
||||
"show": false |
||||
}, |
||||
"showHeader": true, |
||||
"showRowNums": false |
||||
}, |
||||
"pluginVersion": "10.1.0-55406pre", |
||||
"title": "No extensions", |
||||
"type": "table" |
||||
}, |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"fieldConfig": { |
||||
"defaults": { |
||||
"color": { |
||||
"mode": "palette-classic" |
||||
}, |
||||
"custom": { |
||||
"hideFrom": { |
||||
"legend": false, |
||||
"tooltip": false, |
||||
"viz": false |
||||
} |
||||
}, |
||||
"mappings": [] |
||||
}, |
||||
"overrides": [] |
||||
}, |
||||
"gridPos": { |
||||
"h": 8, |
||||
"w": 6, |
||||
"x": 6, |
||||
"y": 8 |
||||
}, |
||||
"id": 4, |
||||
"options": { |
||||
"legend": { |
||||
"displayMode": "list", |
||||
"placement": "bottom", |
||||
"showLegend": true |
||||
}, |
||||
"pieType": "pie", |
||||
"reduceOptions": { |
||||
"calcs": [ |
||||
"lastNotNull" |
||||
], |
||||
"fields": "", |
||||
"values": false |
||||
}, |
||||
"tooltip": { |
||||
"mode": "single", |
||||
"sort": "none" |
||||
} |
||||
}, |
||||
"targets": [ |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"refId": "A", |
||||
"scenarioId": "random_walk", |
||||
"seriesCount": 4 |
||||
}, |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"hide": false, |
||||
"refId": "B", |
||||
"scenarioId": "random_walk", |
||||
"seriesCount": 1 |
||||
} |
||||
], |
||||
"title": "Link with new name", |
||||
"type": "piechart" |
||||
}, |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"fieldConfig": { |
||||
"defaults": { |
||||
"color": { |
||||
"mode": "palette-classic" |
||||
}, |
||||
"custom": { |
||||
"axisCenteredZero": false, |
||||
"axisColorMode": "text", |
||||
"axisLabel": "", |
||||
"axisPlacement": "auto", |
||||
"barAlignment": 0, |
||||
"drawStyle": "line", |
||||
"fillOpacity": 0, |
||||
"gradientMode": "none", |
||||
"hideFrom": { |
||||
"legend": false, |
||||
"tooltip": false, |
||||
"viz": false |
||||
}, |
||||
"lineInterpolation": "linear", |
||||
"lineWidth": 1, |
||||
"pointSize": 5, |
||||
"scaleDistribution": { |
||||
"type": "linear" |
||||
}, |
||||
"showPoints": "auto", |
||||
"spanNulls": false, |
||||
"stacking": { |
||||
"group": "A", |
||||
"mode": "none" |
||||
}, |
||||
"thresholdsStyle": { |
||||
"mode": "off" |
||||
} |
||||
}, |
||||
"mappings": [], |
||||
"thresholds": { |
||||
"mode": "absolute", |
||||
"steps": [ |
||||
{ |
||||
"color": "green", |
||||
"value": null |
||||
}, |
||||
{ |
||||
"color": "red", |
||||
"value": 80 |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"overrides": [] |
||||
}, |
||||
"gridPos": { |
||||
"h": 8, |
||||
"w": 6, |
||||
"x": 0, |
||||
"y": 16 |
||||
}, |
||||
"id": 2, |
||||
"options": { |
||||
"legend": { |
||||
"calcs": [], |
||||
"displayMode": "list", |
||||
"placement": "bottom", |
||||
"showLegend": true |
||||
}, |
||||
"tooltip": { |
||||
"mode": "single", |
||||
"sort": "none" |
||||
} |
||||
}, |
||||
"targets": [ |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"refId": "A", |
||||
"scenarioId": "random_walk", |
||||
"seriesCount": 1 |
||||
}, |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"hide": false, |
||||
"refId": "B", |
||||
"scenarioId": "random_walk", |
||||
"seriesCount": 1 |
||||
} |
||||
], |
||||
"title": "Link with defaults", |
||||
"type": "timeseries" |
||||
} |
||||
], |
||||
"refresh": "", |
||||
"schemaVersion": 38, |
||||
"style": "dark", |
||||
"tags": [], |
||||
"templating": { |
||||
"list": [] |
||||
}, |
||||
"time": { |
||||
"from": "now-6h", |
||||
"to": "now" |
||||
}, |
||||
"timepicker": {}, |
||||
"timezone": "", |
||||
"title": "Link Extensions (onClick)", |
||||
"uid": "dbfb47c5-e5e5-4d28-8ac7-35f349b95946", |
||||
"version": 1, |
||||
"weekStart": "" |
||||
} |
@ -0,0 +1,237 @@ |
||||
{ |
||||
"annotations": { |
||||
"list": [ |
||||
{ |
||||
"builtIn": 1, |
||||
"datasource": { |
||||
"type": "grafana", |
||||
"uid": "-- Grafana --" |
||||
}, |
||||
"enable": true, |
||||
"hide": true, |
||||
"iconColor": "rgba(0, 211, 255, 1)", |
||||
"name": "Annotations & Alerts", |
||||
"type": "dashboard" |
||||
} |
||||
] |
||||
}, |
||||
"editable": true, |
||||
"fiscalYearStartMonth": 0, |
||||
"graphTooltip": 0, |
||||
"id": 1, |
||||
"links": [], |
||||
"liveNow": false, |
||||
"panels": [ |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"fieldConfig": { |
||||
"defaults": { |
||||
"color": { |
||||
"mode": "thresholds" |
||||
}, |
||||
"custom": { |
||||
"align": "auto", |
||||
"cellOptions": { |
||||
"type": "auto" |
||||
}, |
||||
"inspect": false |
||||
}, |
||||
"mappings": [], |
||||
"thresholds": { |
||||
"mode": "absolute", |
||||
"steps": [ |
||||
{ |
||||
"color": "green", |
||||
"value": null |
||||
}, |
||||
{ |
||||
"color": "red", |
||||
"value": 80 |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"overrides": [] |
||||
}, |
||||
"gridPos": { |
||||
"h": 8, |
||||
"w": 6, |
||||
"x": 0, |
||||
"y": 0 |
||||
}, |
||||
"id": 6, |
||||
"options": { |
||||
"cellHeight": "sm", |
||||
"footer": { |
||||
"countRows": false, |
||||
"fields": "", |
||||
"reducer": [ |
||||
"sum" |
||||
], |
||||
"show": false |
||||
}, |
||||
"showHeader": true, |
||||
"showRowNums": false |
||||
}, |
||||
"pluginVersion": "9.5.0-53420pre", |
||||
"title": "No extensions", |
||||
"type": "table" |
||||
}, |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"fieldConfig": { |
||||
"defaults": { |
||||
"color": { |
||||
"mode": "palette-classic" |
||||
}, |
||||
"custom": { |
||||
"hideFrom": { |
||||
"legend": false, |
||||
"tooltip": false, |
||||
"viz": false |
||||
} |
||||
}, |
||||
"mappings": [] |
||||
}, |
||||
"overrides": [] |
||||
}, |
||||
"gridPos": { |
||||
"h": 8, |
||||
"w": 6, |
||||
"x": 6, |
||||
"y": 0 |
||||
}, |
||||
"id": 4, |
||||
"options": { |
||||
"legend": { |
||||
"displayMode": "list", |
||||
"placement": "bottom", |
||||
"showLegend": true |
||||
}, |
||||
"pieType": "pie", |
||||
"reduceOptions": { |
||||
"calcs": [ |
||||
"lastNotNull" |
||||
], |
||||
"fields": "", |
||||
"values": false |
||||
}, |
||||
"tooltip": { |
||||
"mode": "single", |
||||
"sort": "none" |
||||
} |
||||
}, |
||||
"targets": [ |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"refId": "A", |
||||
"scenarioId": "random_walk", |
||||
"seriesCount": 4 |
||||
} |
||||
], |
||||
"title": "Link with new name", |
||||
"type": "piechart" |
||||
}, |
||||
{ |
||||
"datasource": { |
||||
"type": "testdata" |
||||
}, |
||||
"fieldConfig": { |
||||
"defaults": { |
||||
"color": { |
||||
"mode": "palette-classic" |
||||
}, |
||||
"custom": { |
||||
"axisCenteredZero": false, |
||||
"axisColorMode": "text", |
||||
"axisLabel": "", |
||||
"axisPlacement": "auto", |
||||
"barAlignment": 0, |
||||
"drawStyle": "line", |
||||
"fillOpacity": 0, |
||||
"gradientMode": "none", |
||||
"hideFrom": { |
||||
"legend": false, |
||||
"tooltip": false, |
||||
"viz": false |
||||
}, |
||||
"lineInterpolation": "linear", |
||||
"lineWidth": 1, |
||||
"pointSize": 5, |
||||
"scaleDistribution": { |
||||
"type": "linear" |
||||
}, |
||||
"showPoints": "auto", |
||||
"spanNulls": false, |
||||
"stacking": { |
||||
"group": "A", |
||||
"mode": "none" |
||||
}, |
||||
"thresholdsStyle": { |
||||
"mode": "off" |
||||
} |
||||
}, |
||||
"mappings": [], |
||||
"thresholds": { |
||||
"mode": "absolute", |
||||
"steps": [ |
||||
{ |
||||
"color": "green", |
||||
"value": null |
||||
}, |
||||
{ |
||||
"color": "red", |
||||
"value": 80 |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"overrides": [] |
||||
}, |
||||
"gridPos": { |
||||
"h": 8, |
||||
"w": 6, |
||||
"x": 0, |
||||
"y": 8 |
||||
}, |
||||
"id": 2, |
||||
"options": { |
||||
"legend": { |
||||
"calcs": [], |
||||
"displayMode": "list", |
||||
"placement": "bottom", |
||||
"showLegend": true |
||||
}, |
||||
"tooltip": { |
||||
"mode": "single", |
||||
"sort": "none" |
||||
} |
||||
}, |
||||
"title": "Link with defaults", |
||||
"type": "timeseries" |
||||
} |
||||
], |
||||
"refresh": "", |
||||
"schemaVersion": 38, |
||||
"style": "dark", |
||||
"tags": [], |
||||
"templating": { |
||||
"list": [] |
||||
}, |
||||
"time": { |
||||
"from": "now-6h", |
||||
"to": "now" |
||||
}, |
||||
"timepicker": {}, |
||||
"timezone": "", |
||||
"title": "Link Extensions (path)", |
||||
"uid": "d1fbb077-cd44-4738-8c8a-d4e66748b719", |
||||
"version": 3, |
||||
"weekStart": "" |
||||
} |
@ -0,0 +1,19 @@ |
||||
apiVersion: 1 |
||||
|
||||
apps: |
||||
- type: myorg-extensions-app |
||||
org_id: 1 |
||||
org_name: Main Org. |
||||
disabled: false |
||||
- type: myorg-a-app |
||||
org_id: 1 |
||||
org_name: Main Org. |
||||
disabled: false |
||||
- type: myorg-b-app |
||||
org_id: 1 |
||||
org_name: Main Org. |
||||
disabled: false |
||||
- type: myorg-extensionpoint-app |
||||
org_id: 1 |
||||
org_name: Main Org. |
||||
disabled: false |
@ -0,0 +1,5 @@ |
||||
# Custom plugins |
||||
|
||||
Plugins in this directory will be installed when the e2e [test server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) is started. Optionally, you can provision the plugin by adding configuration to the [datasources.yaml](https://github.com/grafana/grafana/blob/extensions/add-e2e-tests/devenv/datasources.yaml) or to the [plugins.yaml](https://github.com/grafana/grafana/blob/extensions/add-e2e-tests/devenv/plugins.yaml). |
||||
|
||||
These plugins are not being built as part of CI. Plugins in this directory are being version controlled, so make sure the bundle size is small. Only use dependencies provided by the runtime (see list of runtime dependencies [here](https://github.com/grafana/plugin-tools/blob/08b67179bdbf8847788c54aadb22654aa1a7c060/packages/create-plugin/templates/common/.config/webpack/webpack.config.ts#L36)). |
@ -0,0 +1,12 @@ |
||||
# App with extension point |
||||
|
||||
This app was initially copied from the [app-with-extension-point](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/app-with-extension-point) 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,141 @@ |
||||
define(['@grafana/data', 'react', '@grafana/ui', '@grafana/runtime'], function (data, React, UI, runtime) { |
||||
'use strict'; |
||||
|
||||
const styles = { |
||||
container: 'main-app-body', |
||||
actions: { button: 'action-button' }, |
||||
modal: { container: 'container', open: 'open-link' }, |
||||
appA: { container: 'a-app-body' }, |
||||
appB: { modal: 'b-app-modal' }, |
||||
}; |
||||
|
||||
function ModalComponent({ onDismiss, title, path }) { |
||||
return React.createElement( |
||||
UI.Modal, |
||||
{ 'data-testid': styles.modal.container, title, isOpen: true, onDismiss }, |
||||
React.createElement( |
||||
UI.VerticalGroup, |
||||
{ spacing: 'sm' }, |
||||
React.createElement('p', null, 'Do you want to proceed in the current tab or open a new tab?') |
||||
), |
||||
React.createElement( |
||||
UI.Modal.ButtonRow, |
||||
null, |
||||
React.createElement(UI.Button, { onClick: onDismiss, fill: 'outline', variant: 'secondary' }, 'Cancel'), |
||||
React.createElement( |
||||
UI.Button, |
||||
{ |
||||
type: 'submit', |
||||
variant: 'secondary', |
||||
onClick: function () { |
||||
window.open(data.locationUtil.assureBaseUrl(path), '_blank'); |
||||
onDismiss(); |
||||
}, |
||||
icon: 'external-link-alt', |
||||
}, |
||||
'Open in new tab' |
||||
), |
||||
React.createElement( |
||||
UI.Button, |
||||
{ |
||||
'data-testid': styles.modal.open, |
||||
type: 'submit', |
||||
variant: 'primary', |
||||
onClick: function () { |
||||
runtime.locationService.push(path); |
||||
}, |
||||
icon: 'apps', |
||||
}, |
||||
'Open' |
||||
) |
||||
) |
||||
); |
||||
} |
||||
|
||||
function ActionComponent({ extensions }) { |
||||
const options = React.useMemo( |
||||
function () { |
||||
return extensions.reduce(function (acc, extension) { |
||||
if (runtime.isPluginExtensionLink(extension)) { |
||||
acc.push({ label: extension.title, title: extension.title, value: extension }); |
||||
} |
||||
return acc; |
||||
}, []); |
||||
}, |
||||
[extensions] |
||||
); |
||||
|
||||
const [selected, setSelected] = React.useState(); |
||||
|
||||
return options.length === 0 |
||||
? React.createElement(UI.Button, null, 'Run default action') |
||||
: React.createElement( |
||||
React.Fragment, |
||||
null, |
||||
React.createElement( |
||||
UI.ButtonGroup, |
||||
null, |
||||
React.createElement( |
||||
UI.ToolbarButton, |
||||
{ |
||||
key: 'default-action', |
||||
variant: 'canvas', |
||||
onClick: function () { |
||||
alert('You triggered the default action'); |
||||
}, |
||||
}, |
||||
'Run default action' |
||||
), |
||||
React.createElement(UI.ButtonSelect, { |
||||
'data-testid': styles.actions.button, |
||||
key: 'select-extension', |
||||
variant: 'canvas', |
||||
options: options, |
||||
onChange: function (e) { |
||||
const extension = e.value; |
||||
if (runtime.isPluginExtensionLink(extension)) { |
||||
if (extension.path) setSelected(extension); |
||||
if (extension.onClick) extension.onClick(); |
||||
} |
||||
}, |
||||
}) |
||||
), |
||||
selected && |
||||
selected.path && |
||||
React.createElement(ModalComponent, { |
||||
title: selected.title, |
||||
path: selected.path, |
||||
onDismiss: function () { |
||||
setSelected(undefined); |
||||
}, |
||||
}) |
||||
); |
||||
} |
||||
|
||||
class RootComponent extends React.PureComponent { |
||||
render() { |
||||
const { extensions } = runtime.getPluginExtensions({ |
||||
extensionPointId: 'plugins/myorg-extensionpoint-app/actions', |
||||
context: {}, |
||||
}); |
||||
|
||||
return React.createElement( |
||||
'div', |
||||
{ 'data-testid': styles.container, style: { marginTop: '5%' } }, |
||||
React.createElement( |
||||
UI.HorizontalGroup, |
||||
{ align: 'flex-start', justify: 'center' }, |
||||
React.createElement( |
||||
UI.HorizontalGroup, |
||||
null, |
||||
React.createElement('span', null, 'Hello Grafana! These are the actions you can trigger from this plugin'), |
||||
React.createElement(ActionComponent, { extensions: extensions }) |
||||
) |
||||
) |
||||
); |
||||
} |
||||
} |
||||
|
||||
const plugin = new data.AppPlugin().setRootPage(RootComponent); |
||||
return { plugin: plugin }; |
||||
}); |
@ -0,0 +1,38 @@ |
||||
{ |
||||
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", |
||||
"type": "app", |
||||
"name": "Extension Point App", |
||||
"id": "myorg-extensionpoint-app", |
||||
"preload": true, |
||||
"info": { |
||||
"keywords": ["app"], |
||||
"description": "Show case how to add an extension point to your 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-extensionpoint-app", |
||||
"role": "Admin", |
||||
"addToNav": true, |
||||
"defaultNav": true |
||||
} |
||||
], |
||||
"dependencies": { |
||||
"grafanaDependency": ">=10.3.3", |
||||
"plugins": [] |
||||
}, |
||||
"generated": { |
||||
"extensions": [] |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
define(['@grafana/data', 'react'], function (data, React) { |
||||
'use strict'; |
||||
|
||||
const styles = { |
||||
container: 'a-app-body', |
||||
}; |
||||
|
||||
class RootComponent extends React.PureComponent { |
||||
render() { |
||||
return React.createElement( |
||||
'div', |
||||
{ 'data-testid': styles.container, className: 'page-container' }, |
||||
'Hello Grafana!' |
||||
); |
||||
} |
||||
} |
||||
|
||||
const plugin = new data.AppPlugin().setRootPage(RootComponent).configureExtensionLink({ |
||||
title: 'Go to A', |
||||
description: 'Navigating to plugin A', |
||||
extensionPointId: 'plugins/myorg-extensionpoint-app/actions', |
||||
path: '/a/myorg-a-app/', |
||||
}); |
||||
|
||||
return { plugin: plugin }; |
||||
}); |
@ -0,0 +1,45 @@ |
||||
{ |
||||
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", |
||||
"type": "app", |
||||
"name": "A App", |
||||
"id": "myorg-a-app", |
||||
"preload": true, |
||||
"info": { |
||||
"keywords": ["app"], |
||||
"description": "Will extend root app with ui extensions", |
||||
"author": { |
||||
"name": "Myorg" |
||||
}, |
||||
"logos": { |
||||
"small": "img/logo.svg", |
||||
"large": "img/logo.svg" |
||||
}, |
||||
"screenshots": [], |
||||
"version": "%VERSION%", |
||||
"updated": "%TODAY%" |
||||
}, |
||||
"includes": [ |
||||
{ |
||||
"type": "page", |
||||
"name": "Default", |
||||
"path": "/a/myorg-a-app", |
||||
"role": "Admin", |
||||
"addToNav": false, |
||||
"defaultNav": false |
||||
} |
||||
], |
||||
"dependencies": { |
||||
"grafanaDependency": ">=10.3.3", |
||||
"plugins": [] |
||||
}, |
||||
"generated": { |
||||
"extensions": [ |
||||
{ |
||||
"extensionPointId": "plugins/myorg-extensionpoint-app/actions", |
||||
"title": "Go to A", |
||||
"description": "Navigating to pluging A", |
||||
"type": "link" |
||||
} |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
define(['react', '@grafana/data'], function (React, data) { |
||||
'use strict'; |
||||
|
||||
class RootComponent extends React.PureComponent { |
||||
render() { |
||||
return React.createElement('div', { className: 'page-container' }, 'Hello Grafana!'); |
||||
} |
||||
} |
||||
|
||||
const modalId = 'b-app-modal'; |
||||
|
||||
const plugin = new data.AppPlugin().setRootPage(RootComponent).configureExtensionLink({ |
||||
title: 'Open from B', |
||||
description: 'Open a modal from plugin B', |
||||
extensionPointId: 'plugins/myorg-extensionpoint-app/actions', |
||||
onClick: function (e, { openModal }) { |
||||
openModal({ |
||||
title: 'Modal from app B', |
||||
body: function () { |
||||
return React.createElement('div', { 'data-testid': modalId }, 'From plugin B'); |
||||
}, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
return { plugin: plugin }; |
||||
}); |
@ -0,0 +1,45 @@ |
||||
{ |
||||
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", |
||||
"type": "app", |
||||
"name": "B App", |
||||
"id": "myorg-b-app", |
||||
"preload": true, |
||||
"info": { |
||||
"keywords": ["app"], |
||||
"description": "Will extend root app with ui extensions", |
||||
"author": { |
||||
"name": "Myorg" |
||||
}, |
||||
"logos": { |
||||
"small": "img/logo.svg", |
||||
"large": "img/logo.svg" |
||||
}, |
||||
"screenshots": [], |
||||
"version": "%VERSION%", |
||||
"updated": "%TODAY%" |
||||
}, |
||||
"includes": [ |
||||
{ |
||||
"type": "page", |
||||
"name": "Default", |
||||
"path": "/a/myorg-b-app", |
||||
"role": "Admin", |
||||
"addToNav": false, |
||||
"defaultNav": false |
||||
} |
||||
], |
||||
"dependencies": { |
||||
"grafanaDependency": ">=10.3.3", |
||||
"plugins": [] |
||||
}, |
||||
"generated": { |
||||
"extensions": [ |
||||
{ |
||||
"extensionPointId": "plugins/myorg-extensionpoint-app/actions", |
||||
"title": "Open from B", |
||||
"description": "Open a modal from plugin B", |
||||
"type": "link" |
||||
} |
||||
] |
||||
} |
||||
} |
@ -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" |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,35 @@ |
||||
import { test, expect } from '@grafana/plugin-e2e'; |
||||
|
||||
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', |
||||
}, |
||||
}; |
||||
|
||||
const pluginId = 'myorg-extensionpoint-app'; |
||||
|
||||
test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { |
||||
await page.goto(`/a/${pluginId}/one`); |
||||
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}/one`); |
||||
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(); |
||||
}); |
@ -0,0 +1,38 @@ |
||||
import { expect, test } from '@grafana/plugin-e2e'; |
||||
|
||||
const panelTitle = 'Link with defaults'; |
||||
const extensionTitle = 'Open from time series...'; |
||||
const testIds = { |
||||
modal: { |
||||
container: 'ape-modal-body', |
||||
}, |
||||
mainPage: { |
||||
container: 'ape-main-page-container', |
||||
}, |
||||
}; |
||||
|
||||
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 }); |
||||
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 }); |
||||
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 }); |
||||
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,23 @@ |
||||
#!/bin/bash |
||||
|
||||
. scripts/grafana-server/variables |
||||
|
||||
LICENSE_PATH="" |
||||
|
||||
if [ "$1" = "enterprise" ]; then |
||||
if [ "$2" != "dev" ] && [ "$2" != "debug" ]; then |
||||
LICENSE_PATH=$2/license.jwt |
||||
else |
||||
LICENSE_PATH=$3/license.jwt |
||||
fi |
||||
fi |
||||
|
||||
if [ "$BASE_URL" != "" ]; then |
||||
echo -e "BASE_URL set, skipping starting server" |
||||
else |
||||
# Start it in the background |
||||
./scripts/grafana-server/start-server $LICENSE_PATH 2>&1 > scripts/grafana-server/server.log & |
||||
./scripts/grafana-server/wait-for-grafana |
||||
fi |
||||
|
||||
PORT=3001 HOST=localhost yarn playwright test |
Loading…
Reference in new issue