From d8ec95e9b1fd58bb521b6c410316e2e4c8acbc61 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 23 Aug 2024 09:00:03 +0200 Subject: [PATCH] E2E: Add support for building test plugins (#91873) * build test apps with webpack * add extensions test app * update e2e tests * remove non-build test apps using amd * use @grafana/plugin-configs rather than create-plugin config * Update e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/usePluginComponents.spec.ts Co-authored-by: Jack Westbrook * Update package.json Co-authored-by: Jack Westbrook * use run dir variable instead of hardcoded path * add dummy licence file * add separate step for building test plugins * support nested plugins * remove react-router-dom from the externals array * remove add_mode dev * lint starlark * pass license path as env variable * fix the path * chore(e2e-plugins): clean up dependencies to match core versions * refactor(e2e-plugins): prefer extending webpack plugins-config * docs(e2e-plugins): add basic info to extensions test plugin readme * update readme * change dir name from custom plugins to test plugins * change root readme * update lockfile --------- Co-authored-by: Jack Westbrook --- .betterer.ts | 1 + .drone.yml | 36 ++- .eslintignore | 2 +- devenv/plugins.yaml | 22 +- e2e/custom-plugins/README.md | 5 - .../app-with-exposed-components/README.md | 22 -- .../app-with-exposed-components/module.js | 28 --- .../app-with-exposed-components/plugin.json | 35 --- .../myorg-componentexposer-app/module.js | 14 -- .../app-with-extension-point/README.md | 12 - .../app-with-extension-point/module.js | 141 ------------ .../app-with-extension-point/plugin.json | 36 --- .../plugins/myorg-a-app/module.js | 26 --- .../plugins/myorg-a-app/plugin.json | 45 ---- .../plugins/myorg-b-app/module.js | 27 --- .../app-with-extensions/README.md | 12 - .../app-with-extensions/module.js | 216 ------------------ .../app-with-extensions/plugin.json | 49 ---- .../extensions/extensionPoints.spec.ts | 22 +- .../extensions/extensions.spec.ts | 8 +- .../extensions/useExposedComponent.spec.ts | 4 +- .../extensions/usePluginComponents.spec.ts | 11 + .../as-admin-user/extensions/utils.ts | 10 + e2e/test-plugins/README.md | 33 +++ .../frontend-sandbox-app-test/img/logo.svg | 0 .../frontend-sandbox-app-test/module.js | 0 .../frontend-sandbox-app-test/plugin.json | 0 .../img/logo.svg | 0 .../module.js | 0 .../plugin.json | 0 .../frontend-sandbox-panel-test/img/logo.svg | 0 .../frontend-sandbox-panel-test/module.js | 0 .../frontend-sandbox-panel-test/plugin.json | 0 .../grafana-extensionstest-app/.gitignore | 39 ++++ .../grafana-extensionstest-app/CHANGELOG.md | 1 + .../grafana-extensionstest-app/README.md | 35 +++ .../components/ActionButton/ActionButton.tsx | 100 ++++++++ .../components/ActionButton/index.ts | 1 + .../components/App/App.tsx | 19 ++ .../components/App/index.tsx | 1 + .../components/AppConfig/AppConfig.tsx | 135 +++++++++++ .../components/AppConfig/index.tsx | 1 + .../components/QueryModal/QueryModal.tsx | 45 ++++ .../components/QueryModal/index.ts | 1 + .../components/testIds.ts | 36 +++ .../grafana-extensionstest-app/constants.ts | 9 + .../grafana-extensionstest-app}/img/logo.svg | 0 .../grafana-extensionstest-app/module.tsx | 86 +++++++ .../grafana-extensionstest-app/package.json | 48 ++++ .../pages/AddedComponents.tsx | 26 +++ .../pages/ExposedComponents.tsx | 24 ++ .../pages/LegacyAPIs.tsx | 44 ++++ .../pages/index.tsx | 3 + .../grafana-extensionstest-app/plugin.json | 59 +++++ .../components/App/App.tsx | 13 ++ .../components/App/index.tsx | 1 + .../img/logo.svg | 0 .../grafana-extensionexample1-app/module.tsx | 17 ++ .../plugin.json | 6 +- .../grafana-extensionexample1-app/testIds.ts | 16 ++ .../components/App/App.tsx | 8 + .../components/App/index.tsx | 1 + .../img/logo.svg | 1 + .../grafana-extensionexample2-app/module.tsx | 32 +++ .../plugin.json | 18 +- .../grafana-extensionexample2-app/testIds.ts | 18 ++ .../grafana-extensionstest-app/tsconfig.json | 8 + .../utils/utils.routing.ts | 6 + .../grafana-extensionstest-app/utils/utils.ts | 5 + .../webpack.config.ts | 44 ++++ package.json | 5 +- packages/grafana-plugin-configs/utils.ts | 30 ++- .../grafana-plugin-configs/webpack.config.ts | 1 - scripts/drone/pipelines/build.star | 2 + scripts/drone/steps/lib.star | 22 ++ scripts/grafana-server/custom.ini | 4 + scripts/grafana-server/start-server | 13 +- yarn.lock | 64 ++++++ 78 files changed, 1143 insertions(+), 722 deletions(-) delete mode 100644 e2e/custom-plugins/README.md delete mode 100644 e2e/custom-plugins/app-with-exposed-components/README.md delete mode 100644 e2e/custom-plugins/app-with-exposed-components/module.js delete mode 100644 e2e/custom-plugins/app-with-exposed-components/plugin.json delete mode 100644 e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/module.js delete mode 100644 e2e/custom-plugins/app-with-extension-point/README.md delete mode 100644 e2e/custom-plugins/app-with-extension-point/module.js delete mode 100644 e2e/custom-plugins/app-with-extension-point/plugin.json delete mode 100644 e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/module.js delete mode 100644 e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/plugin.json delete mode 100644 e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/module.js delete mode 100644 e2e/custom-plugins/app-with-extensions/README.md delete mode 100644 e2e/custom-plugins/app-with-extensions/module.js delete mode 100644 e2e/custom-plugins/app-with-extensions/plugin.json create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/usePluginComponents.spec.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/utils.ts create mode 100644 e2e/test-plugins/README.md rename e2e/{custom-plugins => test-plugins}/frontend-sandbox-app-test/img/logo.svg (100%) rename e2e/{custom-plugins => test-plugins}/frontend-sandbox-app-test/module.js (100%) rename e2e/{custom-plugins => test-plugins}/frontend-sandbox-app-test/plugin.json (100%) rename e2e/{custom-plugins => test-plugins}/frontend-sandbox-datasource-test/img/logo.svg (100%) rename e2e/{custom-plugins => test-plugins}/frontend-sandbox-datasource-test/module.js (100%) rename e2e/{custom-plugins => test-plugins}/frontend-sandbox-datasource-test/plugin.json (100%) rename e2e/{custom-plugins => test-plugins}/frontend-sandbox-panel-test/img/logo.svg (100%) rename e2e/{custom-plugins => test-plugins}/frontend-sandbox-panel-test/module.js (100%) rename e2e/{custom-plugins => test-plugins}/frontend-sandbox-panel-test/plugin.json (100%) create mode 100644 e2e/test-plugins/grafana-extensionstest-app/.gitignore create mode 100644 e2e/test-plugins/grafana-extensionstest-app/CHANGELOG.md create mode 100644 e2e/test-plugins/grafana-extensionstest-app/README.md create mode 100644 e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts create mode 100644 e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/components/App/index.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/AppConfig.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/index.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts create mode 100644 e2e/test-plugins/grafana-extensionstest-app/components/testIds.ts create mode 100644 e2e/test-plugins/grafana-extensionstest-app/constants.ts rename e2e/{custom-plugins/app-with-exposed-components => test-plugins/grafana-extensionstest-app}/img/logo.svg (100%) create mode 100644 e2e/test-plugins/grafana-extensionstest-app/module.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/package.json create mode 100644 e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/pages/LegacyAPIs.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/plugin.json create mode 100644 e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx rename e2e/{custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app => test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app}/img/logo.svg (100%) create mode 100644 e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx rename e2e/{custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app => test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app}/plugin.json (86%) create mode 100644 e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/testIds.ts create mode 100644 e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx create mode 100644 e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg create mode 100644 e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx rename e2e/{custom-plugins/app-with-extension-point/plugins/myorg-b-app => test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app}/plugin.json (68%) create mode 100644 e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/testIds.ts create mode 100644 e2e/test-plugins/grafana-extensionstest-app/tsconfig.json create mode 100644 e2e/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts create mode 100644 e2e/test-plugins/grafana-extensionstest-app/utils/utils.ts create mode 100644 e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts diff --git a/.betterer.ts b/.betterer.ts index 269ef0d4dd0..6b92c439de6 100644 --- a/.betterer.ts +++ b/.betterer.ts @@ -10,6 +10,7 @@ const eslintPathsToIgnore = [ 'public/app/angular', // will be removed in Grafana 11 'public/app/plugins/panel/graph', // will be removed alongside angular 'public/app/plugins/panel/table-old', // will be removed alongside angular + 'e2e/test-plugins', ]; // Avoid using functions that report the position of the issues, as this causes a lot of merge conflicts diff --git a/.drone.yml b/.drone.yml index 10cd4270992..57e6b2857d8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -628,6 +628,14 @@ steps: volumes: - name: docker path: /var/run/docker.sock +- commands: + - yarn e2e:plugin:build + depends_on: + - yarn-install + environment: + NODE_OPTIONS: --max_old_space_size=8192 + image: node:20.9.0-alpine + name: build-test-plugins - commands: - apk add --update tar bash - mkdir grafana @@ -646,6 +654,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -654,6 +663,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite scenes/dashboards-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -662,6 +672,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -670,6 +681,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite scenes/smoke-tests-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -678,6 +690,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite panels-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -686,6 +699,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite scenes/panels-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -694,6 +708,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite various-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -702,6 +717,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite scenes/various-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -738,6 +754,7 @@ steps: - yarn e2e:playwright depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server PORT: "3001" @@ -2035,6 +2052,14 @@ steps: volumes: - name: docker path: /var/run/docker.sock +- commands: + - yarn e2e:plugin:build + depends_on: + - yarn-install + environment: + NODE_OPTIONS: --max_old_space_size=8192 + image: node:20.9.0-alpine + name: build-test-plugins - commands: - apk add --update tar bash - mkdir grafana @@ -2053,6 +2078,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -2061,6 +2087,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite scenes/dashboards-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -2069,6 +2096,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -2077,6 +2105,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite scenes/smoke-tests-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -2085,6 +2114,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite panels-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -2093,6 +2123,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite scenes/panels-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -2101,6 +2132,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite various-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -2109,6 +2141,7 @@ steps: - ./bin/build e2e-tests --port 3001 --suite scenes/various-suite depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 @@ -2145,6 +2178,7 @@ steps: - yarn e2e:playwright depends_on: - grafana-server + - build-test-plugins environment: HOST: grafana-server PORT: "3001" @@ -6074,6 +6108,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: cd0bc27b34a09de191974f360d43b55324bd88d20c4fe92f7c41df56394fc25a +hmac: 7c752913b444e0efe410d5a8a0d300e1b4d48d2cac8df602c35314bc62b7ac3c ... diff --git a/.eslintignore b/.eslintignore index 95ace0ec535..1b83fc33e02 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,7 +13,7 @@ node_modules /public/lib/monaco /scripts/grafana-server/tmp vendor -e2e/custom-plugins +e2e/test-plugins playwright-report # TS generate from cue by cuetsy diff --git a/devenv/plugins.yaml b/devenv/plugins.yaml index 097428a6ca9..7abef7b8789 100644 --- a/devenv/plugins.yaml +++ b/devenv/plugins.yaml @@ -1,27 +1,19 @@ apiVersion: 1 apps: - - type: myorg-extensions-app + - type: grafana-extensionstest-app org_id: 1 org_name: Main Org. disabled: false - - type: myorg-a-app + jsonData: + apiUrl: http://default-url.com + secureJsonData: + apiKey: secret-key + - type: grafana-extensionexample1-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 - - type: myorg-componentconsumer-app - org_id: 1 - org_name: Main Org. - disabled: false - - type: myorg-componentexposer-app + - type: grafana-extensionexample2-app org_id: 1 org_name: Main Org. disabled: false diff --git a/e2e/custom-plugins/README.md b/e2e/custom-plugins/README.md deleted file mode 100644 index 9fef8ba9aaf..00000000000 --- a/e2e/custom-plugins/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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)). diff --git a/e2e/custom-plugins/app-with-exposed-components/README.md b/e2e/custom-plugins/app-with-exposed-components/README.md deleted file mode 100644 index 03590601496..00000000000 --- a/e2e/custom-plugins/app-with-exposed-components/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# App with exposed components - -This directory contains two apps - `myorg-componentconsumer-app` and `myorg-componentexposer-app` which is nested inside `myorg-componentconsumer-app`. - -`myorg-componentconsumer-app` exposes a simple React component using the [`exposeComponent`](https://grafana.com/developers/plugin-tools/reference/ui-extensions#exposecomponent) api. `myorg-componentconsumer-app` in turn, consumes this compoment using the [`https://grafana.com/developers/plugin-tools/reference/ui-extensions#useplugincomponent`](https://grafana.com/developers/plugin-tools/reference/ui-extensions#useplugincomponent) hook. - -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 -``` - -or - -``` -PORT=3000 ./scripts/grafana-server/start-server -yarn start -yarn e2e -``` diff --git a/e2e/custom-plugins/app-with-exposed-components/module.js b/e2e/custom-plugins/app-with-exposed-components/module.js deleted file mode 100644 index b73fbd617ce..00000000000 --- a/e2e/custom-plugins/app-with-exposed-components/module.js +++ /dev/null @@ -1,28 +0,0 @@ -define(['@grafana/data', '@grafana/runtime', 'react'], function (grafanaData, grafanaRuntime, React) { - var AppPlugin = grafanaData.AppPlugin; - var usePluginComponent = grafanaRuntime.usePluginComponent; - - var MyComponent = function () { - var plugin = usePluginComponent('myorg-componentexposer-app/reusable-component/v1'); - var TestComponent = plugin.component; - var isLoading = plugin.isLoading; - - if (!TestComponent) { - return null; - } - - return React.createElement( - React.Fragment, - null, - React.createElement('div', null, 'Exposed component:'), - isLoading ? 'Loading..' : React.createElement(TestComponent, { name: 'World' }) - ); - }; - - var App = function () { - return React.createElement('div', null, 'Hello Grafana!', React.createElement(MyComponent, null)); - }; - - var plugin = new AppPlugin().setRootPage(App); - return { plugin: plugin }; -}); diff --git a/e2e/custom-plugins/app-with-exposed-components/plugin.json b/e2e/custom-plugins/app-with-exposed-components/plugin.json deleted file mode 100644 index caf74b2d1a8..00000000000 --- a/e2e/custom-plugins/app-with-exposed-components/plugin.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", - "type": "app", - "name": "Extensions exposed component App", - "id": "myorg-componentconsumer-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-08-09" - }, - "includes": [ - { - "type": "page", - "name": "Default", - "path": "/a/myorg-componentconsumer-app", - "role": "Admin", - "addToNav": true, - "defaultNav": true - } - ], - "dependencies": { - "grafanaDependency": ">=10.3.3", - "plugins": [] - } -} diff --git a/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/module.js b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/module.js deleted file mode 100644 index 4ccaba8ca36..00000000000 --- a/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/module.js +++ /dev/null @@ -1,14 +0,0 @@ -define(['@grafana/data', 'module', 'react'], function (grafanaData, amdModule, React) { - const plugin = new grafanaData.AppPlugin().exposeComponent({ - id: 'myorg-componentexposer-app/reusable-component/v1', - title: 'Reusable component', - description: 'A component that can be reused by other app plugins.', - component: function ({ name }) { - return React.createElement('div', { 'data-testid': 'exposed-component' }, 'Hello ', name, '!'); - }, - }); - - return { - plugin: plugin, - }; -}); diff --git a/e2e/custom-plugins/app-with-extension-point/README.md b/e2e/custom-plugins/app-with-extension-point/README.md deleted file mode 100644 index 99b656fa109..00000000000 --- a/e2e/custom-plugins/app-with-extension-point/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# 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 -``` diff --git a/e2e/custom-plugins/app-with-extension-point/module.js b/e2e/custom-plugins/app-with-extension-point/module.js deleted file mode 100644 index f08f08a2587..00000000000 --- a/e2e/custom-plugins/app-with-extension-point/module.js +++ /dev/null @@ -1,141 +0,0 @@ -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 }; -}); diff --git a/e2e/custom-plugins/app-with-extension-point/plugin.json b/e2e/custom-plugins/app-with-extension-point/plugin.json deleted file mode 100644 index 3a4ab08faa2..00000000000 --- a/e2e/custom-plugins/app-with-extension-point/plugin.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$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": [] - }, - "extensions": [] -} diff --git a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/module.js b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/module.js deleted file mode 100644 index 055db7959fb..00000000000 --- a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/module.js +++ /dev/null @@ -1,26 +0,0 @@ -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 }; -}); diff --git a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/plugin.json b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/plugin.json deleted file mode 100644 index a37ec4e1710..00000000000 --- a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/plugin.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "$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" - } - ] - } -} diff --git a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/module.js b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/module.js deleted file mode 100644 index 03c7468e6ae..00000000000 --- a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/module.js +++ /dev/null @@ -1,27 +0,0 @@ -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 }; -}); diff --git a/e2e/custom-plugins/app-with-extensions/README.md b/e2e/custom-plugins/app-with-extensions/README.md deleted file mode 100644 index 62ceac9ced4..00000000000 --- a/e2e/custom-plugins/app-with-extensions/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# 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 -``` diff --git a/e2e/custom-plugins/app-with-extensions/module.js b/e2e/custom-plugins/app-with-extensions/module.js deleted file mode 100644 index 82469a3e211..00000000000 --- a/e2e/custom-plugins/app-with-extensions/module.js +++ /dev/null @@ -1,216 +0,0 @@ -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 }; -}); diff --git a/e2e/custom-plugins/app-with-extensions/plugin.json b/e2e/custom-plugins/app-with-extensions/plugin.json deleted file mode 100644 index d54f150b836..00000000000 --- a/e2e/custom-plugins/app-with-extensions/plugin.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "$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" - } - ] -} diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts index bdd51f67d7e..902adc04adf 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts @@ -1,5 +1,7 @@ import { test, expect } from '@grafana/plugin-e2e'; +import { ensureExtensionRegistryIsPopulated } from './utils'; + const testIds = { container: 'main-app-body', actions: { @@ -14,13 +16,18 @@ const testIds = { }, appB: { modal: 'b-app-modal', + reusableComponent: 'b-app-configure-extension-component', + }, + legacyAPIPage: { + container: 'data-testid pg-two-container', }, }; -const pluginId = 'myorg-extensionpoint-app'; +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}/one`); + 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(); @@ -28,7 +35,16 @@ test('should extend the actions menu with a link to a-app plugin', async ({ page }); test('should extend the actions menu with a command triggered from b-app plugin', async ({ page }) => { - await page.goto(`/a/${pluginId}/one`); + await page.goto(`/a/${pluginId}/legacy-apis`); + await ensureExtensionRegistryIsPopulated(page); + await expect( + page.getByTestId(testIds.legacyAPIPage.container).getByTestId(testIds.appB.reusableComponent) + ).toHaveText('Hello World!'); +}); + +test('should extend main app with component extension from app B', async ({ page }) => { + await page.goto(`/a/${pluginId}/legacy-apis`); + await ensureExtensionRegistryIsPopulated(page); await page.getByTestId(testIds.actions.button).click(); await page.getByTestId(testIds.container).getByText('Open from B').click(); await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts index 71ada2f4ee5..b87b4eb0bca 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts @@ -1,5 +1,8 @@ +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 = { @@ -7,7 +10,7 @@ const testIds = { container: 'ape-modal-body', }, mainPage: { - container: 'ape-main-page-container', + container: 'main-app-body', }, }; @@ -16,6 +19,7 @@ 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(); @@ -23,6 +27,7 @@ test('should add link extension (path) with defaults to time series panel', asyn 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"'); @@ -32,6 +37,7 @@ test('should add link extension (onclick) with new title to pie chart panel', as const panelTitle = 'Link with new name'; const extensionTitle = 'Open from piechart'; const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + await ensureExtensionRegistryIsPopulated(page); const panel = await dashboardPage.getPanelByTitle(panelTitle); await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts index 9a01139b445..20676945698 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts @@ -1,9 +1,9 @@ import { test, expect } from '@grafana/plugin-e2e'; -const pluginId = 'myorg-componentconsumer-app'; +const pluginId = 'grafana-extensionstest-app'; const exposedComponentTestId = 'exposed-component'; test('should display component exposed by another app', async ({ page }) => { - await page.goto(`/a/${pluginId}`); + await page.goto(`/a/${pluginId}/exposed-components`); await expect(await page.getByTestId(exposedComponentTestId)).toHaveText('Hello World!'); }); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/usePluginComponents.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/usePluginComponents.spec.ts new file mode 100644 index 00000000000..0b1be323949 --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/usePluginComponents.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const pluginId = 'grafana-extensionstest-app'; +const exposedComponentTestId = 'exposed-component'; + +test('should render component with usePluginComponents hook', async ({ page }) => { + await page.goto(`/a/${pluginId}/added-components`); + await expect( + page.getByTestId('data-testid pg-added-components-container').getByTestId('b-app-add-component') + ).toHaveText('Hello World!'); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/utils.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/utils.ts new file mode 100644 index 00000000000..1d134f52cab --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/utils.ts @@ -0,0 +1,10 @@ +import { Page } from '@playwright/test'; + +import { selectors } from '@grafana/e2e-selectors'; + +export async function ensureExtensionRegistryIsPopulated(page: Page) { + // Due to these plugins using the old getter extensions api we need to force a refresh by navigating home then back + // to guarantee the extensions are available to the plugin before we interact with the page. + await page.getByTestId(selectors.components.Breadcrumbs.breadcrumb('Home')).click(); + await page.goBack(); +} diff --git a/e2e/test-plugins/README.md b/e2e/test-plugins/README.md new file mode 100644 index 00000000000..c360fa72496 --- /dev/null +++ b/e2e/test-plugins/README.md @@ -0,0 +1,33 @@ +# Test plugins + +The [e2e test server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) automatically scans and looks for plugins in this directory. + +### To add a new test plugin: + +1. If provisioning is required you may update the YAML config file in [`/devenv`](https://github.com/grafana/grafana/tree/main/devenv). +2. Add the plugin ID to the `allow_loading_unsigned_plugins` setting in the test server's [configuration file](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/custom.ini). + +### Building a test plugin with webpack + +If you wish to build a test plugin with webpack, you may take a look at how the [grafana-extensionstest-app](./grafana-extensionstest-app/) is wired. A few things to keep in mind: + +- the package name needs to be prefixed with `@test-plugins/` +- extend the webpack config from [`@grafana/plugin-configs`](../../packages/grafana-plugin-configs/) and use custom webpack config to only copy the necessary files (see example [here](./grafana-extensionstest-app/webpack.config.ts)) +- keep dependency versions in sync with what's in core + +#### Local development + +1: Install frontend dependencies: +`yarn install --immutable` + +2: Build and watch the core frontend +`yarn start` + +3: Build and watch the test plugins +`yarn e2e:plugin:build:dev` + +4: Build the backend +`make build-go` + +5: Start the Grafana e2e test server with the provisioned test plugin +`PORT=3000 ./scripts/grafana-server/start-server` diff --git a/e2e/custom-plugins/frontend-sandbox-app-test/img/logo.svg b/e2e/test-plugins/frontend-sandbox-app-test/img/logo.svg similarity index 100% rename from e2e/custom-plugins/frontend-sandbox-app-test/img/logo.svg rename to e2e/test-plugins/frontend-sandbox-app-test/img/logo.svg diff --git a/e2e/custom-plugins/frontend-sandbox-app-test/module.js b/e2e/test-plugins/frontend-sandbox-app-test/module.js similarity index 100% rename from e2e/custom-plugins/frontend-sandbox-app-test/module.js rename to e2e/test-plugins/frontend-sandbox-app-test/module.js diff --git a/e2e/custom-plugins/frontend-sandbox-app-test/plugin.json b/e2e/test-plugins/frontend-sandbox-app-test/plugin.json similarity index 100% rename from e2e/custom-plugins/frontend-sandbox-app-test/plugin.json rename to e2e/test-plugins/frontend-sandbox-app-test/plugin.json diff --git a/e2e/custom-plugins/frontend-sandbox-datasource-test/img/logo.svg b/e2e/test-plugins/frontend-sandbox-datasource-test/img/logo.svg similarity index 100% rename from e2e/custom-plugins/frontend-sandbox-datasource-test/img/logo.svg rename to e2e/test-plugins/frontend-sandbox-datasource-test/img/logo.svg diff --git a/e2e/custom-plugins/frontend-sandbox-datasource-test/module.js b/e2e/test-plugins/frontend-sandbox-datasource-test/module.js similarity index 100% rename from e2e/custom-plugins/frontend-sandbox-datasource-test/module.js rename to e2e/test-plugins/frontend-sandbox-datasource-test/module.js diff --git a/e2e/custom-plugins/frontend-sandbox-datasource-test/plugin.json b/e2e/test-plugins/frontend-sandbox-datasource-test/plugin.json similarity index 100% rename from e2e/custom-plugins/frontend-sandbox-datasource-test/plugin.json rename to e2e/test-plugins/frontend-sandbox-datasource-test/plugin.json diff --git a/e2e/custom-plugins/frontend-sandbox-panel-test/img/logo.svg b/e2e/test-plugins/frontend-sandbox-panel-test/img/logo.svg similarity index 100% rename from e2e/custom-plugins/frontend-sandbox-panel-test/img/logo.svg rename to e2e/test-plugins/frontend-sandbox-panel-test/img/logo.svg diff --git a/e2e/custom-plugins/frontend-sandbox-panel-test/module.js b/e2e/test-plugins/frontend-sandbox-panel-test/module.js similarity index 100% rename from e2e/custom-plugins/frontend-sandbox-panel-test/module.js rename to e2e/test-plugins/frontend-sandbox-panel-test/module.js diff --git a/e2e/custom-plugins/frontend-sandbox-panel-test/plugin.json b/e2e/test-plugins/frontend-sandbox-panel-test/plugin.json similarity index 100% rename from e2e/custom-plugins/frontend-sandbox-panel-test/plugin.json rename to e2e/test-plugins/frontend-sandbox-panel-test/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/.gitignore b/e2e/test-plugins/grafana-extensionstest-app/.gitignore new file mode 100644 index 00000000000..1d915e04134 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/.gitignore @@ -0,0 +1,39 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +node_modules/ + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Compiled binary addons (https://nodejs.org/api/addons.html) +dist/ +artifacts/ +work/ +ci/ + +# e2e test directories +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ + +# Editor +.idea + +.eslintcache diff --git a/e2e/test-plugins/grafana-extensionstest-app/CHANGELOG.md b/e2e/test-plugins/grafana-extensionstest-app/CHANGELOG.md new file mode 100644 index 00000000000..825c32f0d03 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/e2e/test-plugins/grafana-extensionstest-app/README.md b/e2e/test-plugins/grafana-extensionstest-app/README.md new file mode 100644 index 00000000000..02b4f9d4690 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/README.md @@ -0,0 +1,35 @@ +# Extensions test plugins + +This is an app plugin containing nested app plugins that are used for testing the plugins ui extensions APIs. + +Further reading: + +- [Plugin Ui Extensions docs](https://grafana.com/developers/plugin-tools/how-to-guides/ui-extensions/) +- [Plugin E2e testing docs](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/introduction) + +## Build + +To build this plugin run `yarn e2e:plugin:build`. + +## Development + +1: Install frontend dependencies: +`yarn install --immutable` + +2: Build and watch the core frontend +`yarn start` + +3: Build and watch the test plugins +`yarn e2e:plugin:build:dev` + +4: Build the backend +`make build-go` + +5: Start the Grafana e2e test server with the provisioned test plugin +`PORT=3000 ./scripts/grafana-server/start-server` + +Note that this plugin extends the `@grafana/plugin-configs` configs which is why it has no src directory and uses a custom webpack config to copy necessary files. + +## Run Playwright tests + +- `yarn e2e:playwright` diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx new file mode 100644 index 00000000000..62758171301 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx @@ -0,0 +1,100 @@ +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 { ReactElement, useMemo, useState } from 'react'; + +type Props = { + extensions: PluginExtension[]; +}; + +export function ActionButton(props: Props): ReactElement { + const options = useExtensionsAsOptions(props.extensions); + const [extension, setExtension] = useState(); + + if (options.length === 0) { + return ; + } + + return ( + <> + + alert('You triggered the default action')}> + Run default action + + { + const extension = option.value; + + if (isPluginExtensionLink(extension)) { + if (extension.path) { + return setExtension(extension); + } + if (extension.onClick) { + return extension.onClick(); + } + } + }} + /> + + {extension && extension?.path && ( + setExtension(undefined)} /> + )} + + ); +} + +function useExtensionsAsOptions(extensions: PluginExtension[]): Array> { + return useMemo(() => { + return extensions.reduce((options: Array>, extension) => { + if (isPluginExtensionLink(extension)) { + options.push({ + label: extension.title, + title: extension.title, + value: extension, + }); + } + return options; + }, []); + }, [extensions]); +} + +type LinkModelProps = { + onDismiss: () => void; + title: string; + path: string; +}; + +export function LinkModal(props: LinkModelProps): ReactElement { + const { onDismiss, title, path } = props; + const openInNewTab = () => { + global.open(locationUtil.assureBaseUrl(path), '_blank'); + onDismiss(); + }; + + const openInCurrentTab = () => locationService.push(path); + + return ( + + +

Do you want to proceed in the current tab or open a new tab?

+
+ + + + + +
+ ); +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts b/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts new file mode 100644 index 00000000000..ee0c64cd96c --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts @@ -0,0 +1 @@ +export { ActionButton } from './ActionButton'; diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx new file mode 100644 index 00000000000..23dc821f4be --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx @@ -0,0 +1,19 @@ +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'; + +export function App(props: AppRootProps) { + return ( +
+ + } /> + } /> + } /> + + } /> + +
+ ); +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/App/index.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/App/index.tsx new file mode 100644 index 00000000000..ac7ba3b3a24 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/components/App/index.tsx @@ -0,0 +1 @@ +export * from './App'; diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/AppConfig.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/AppConfig.tsx new file mode 100644 index 00000000000..44e2d4415c0 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/AppConfig.tsx @@ -0,0 +1,135 @@ +import { ChangeEvent, useState } from 'react'; +import { lastValueFrom } from 'rxjs'; +import { css } from '@emotion/css'; +import { AppPluginMeta, GrafanaTheme2, PluginConfigPageProps, PluginMeta } from '@grafana/data'; +import { getBackendSrv } from '@grafana/runtime'; +import { Button, Field, FieldSet, Input, SecretInput, useStyles2 } from '@grafana/ui'; +import { testIds } from '../testIds'; + +export type AppPluginSettings = { + apiUrl?: string; +}; + +type State = { + // The URL to reach our custom API. + apiUrl: string; + // Tells us if the API key secret is set. + isApiKeySet: boolean; + // A secret key for our custom API. + apiKey: string; +}; + +export interface AppConfigProps extends PluginConfigPageProps> {} + +export const AppConfig = ({ plugin }: AppConfigProps) => { + const s = useStyles2(getStyles); + const { enabled, pinned, jsonData, secureJsonFields } = plugin.meta; + const [state, setState] = useState({ + apiUrl: jsonData?.apiUrl || '', + apiKey: '', + isApiKeySet: Boolean(secureJsonFields?.apiKey), + }); + + const onResetApiKey = () => + setState({ + ...state, + apiKey: '', + isApiKeySet: false, + }); + + const onChange = (event: ChangeEvent) => { + setState({ + ...state, + [event.target.name]: event.target.value.trim(), + }); + }; + + return ( +
+
+ + + + + + + + +
+ +
+
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + colorWeak: css` + color: ${theme.colors.text.secondary}; + `, + marginTop: css` + margin-top: ${theme.spacing(3)}; + `, +}); + +const updatePluginAndReload = async (pluginId: string, data: Partial>) => { + try { + await updatePlugin(pluginId, data); + + // Reloading the page as the changes made here wouldn't be propagated to the actual plugin otherwise. + // This is not ideal, however unfortunately currently there is no supported way for updating the plugin state. + window.location.reload(); + } catch (e) { + console.error('Error while updating the plugin', e); + } +}; + +export const updatePlugin = async (pluginId: string, data: Partial) => { + const response = await getBackendSrv().fetch({ + url: `/api/plugins/${pluginId}/settings`, + method: 'POST', + data, + }); + + return lastValueFrom(response); +}; diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/index.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/index.tsx new file mode 100644 index 00000000000..1dba18f08fd --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/components/AppConfig/index.tsx @@ -0,0 +1 @@ +export * from './AppConfig'; diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx b/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx new file mode 100644 index 00000000000..f31e3b131b3 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx @@ -0,0 +1,45 @@ +import { DataQuery } from '@grafana/data'; +import { Button, FilterPill, Modal, Stack } from '@grafana/ui'; +import { testIds } from '../testIds'; +import { ReactElement, useState } from 'react'; +import { selectQuery } from '../../utils/utils'; + +type Props = { + targets: DataQuery[] | undefined; + onDismiss?: () => void; +}; + +export function QueryModal(props: Props): ReactElement { + const { targets = [], onDismiss } = props; + const [selected, setSelected] = useState(targets[0]); + + return ( +
+

Please select the query you would like to use to create "something" in the plugin.

+ + {targets.map((query) => ( + setSelected(query)} + /> + ))} + + + + + +
+ ); +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts b/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts new file mode 100644 index 00000000000..d0f61309a21 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts @@ -0,0 +1 @@ +export { QueryModal } from './QueryModal'; diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/testIds.ts b/e2e/test-plugins/grafana-extensionstest-app/components/testIds.ts new file mode 100644 index 00000000000..409baf608c9 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/components/testIds.ts @@ -0,0 +1,36 @@ +export const testIds = { + container: 'main-app-body', + actions: { + button: 'action-button', + }, + modal: { + container: 'container', + open: 'open-link', + }, + appA: { + container: 'a-app-body', + }, + appB: { + modal: 'b-app-modal', + }, + appConfig: { + container: 'data-testid ac-container', + apiKey: 'data-testid ac-api-key', + apiUrl: 'data-testid ac-api-url', + submit: 'data-testid ac-submit-form', + }, + pageOne: { + container: 'data-testid pg-one-container', + navigateToFour: 'data-testid navigate-to-four', + }, + pageTwo: { + container: 'data-testid pg-two-container', + }, + addedComponentsPage: { + container: 'data-testid pg-added-components-container', + }, + pageFour: { + container: 'data-testid pg-four-container', + navigateBack: 'data-testid navigate-back', + }, +}; diff --git a/e2e/test-plugins/grafana-extensionstest-app/constants.ts b/e2e/test-plugins/grafana-extensionstest-app/constants.ts new file mode 100644 index 00000000000..e259887ca99 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/constants.ts @@ -0,0 +1,9 @@ +import pluginJson from './plugin.json'; + +export const PLUGIN_BASE_URL = `/a/${pluginJson.id}`; + +export enum ROUTES { + LegacyAPIs = 'legacy-apis', + ExposedComponents = 'exposed-components', + AddedComponents = 'added-components', +} diff --git a/e2e/custom-plugins/app-with-exposed-components/img/logo.svg b/e2e/test-plugins/grafana-extensionstest-app/img/logo.svg similarity index 100% rename from e2e/custom-plugins/app-with-exposed-components/img/logo.svg rename to e2e/test-plugins/grafana-extensionstest-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/module.tsx b/e2e/test-plugins/grafana-extensionstest-app/module.tsx new file mode 100644 index 00000000000..f98d6629e48 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/module.tsx @@ -0,0 +1,86 @@ +import { AppPlugin, PluginExtensionPanelContext, PluginExtensionPoints } from '@grafana/data'; +import { App } from './components/App'; +import { QueryModal } from './components/QueryModal'; +import { selectQuery } from './utils/utils'; +import pluginJson from './plugin.json'; + +export const plugin = new AppPlugin<{}>() + .setRootPage(App) + .configureExtensionLink({ + title: 'Open from time series or pie charts (path)', + description: 'This link will only be visible on time series and pie charts', + extensionPointId: PluginExtensionPoints.DashboardPanelMenu, + path: `/a/${pluginJson.id}/`, + configure: (context) => { + // Will only be visible for the Link Extensions dashboard + if (context?.dashboard?.title !== 'Link Extensions (path)') { + return undefined; + } + + switch (context?.pluginId) { + case 'timeseries': + return {}; // Does not apply any overrides + case 'piechart': + return { + title: `Open from ${context.pluginId}`, + }; + + default: + // By returning undefined the extension will be hidden + return undefined; + } + }, + }) + .configureExtensionLink({ + title: 'Open from time series or pie charts (onClick)', + description: 'This link will only be visible on time series and pie charts', + extensionPointId: PluginExtensionPoints.DashboardPanelMenu, + onClick: (_, { openModal, context }) => { + const targets = context?.targets ?? []; + const title = context?.title; + + if (!isSupported(context)) { + return; + } + + // Show a modal to display a UI for selecting between the available queries (targets) + // in case there are more available. + if (targets.length > 1) { + return openModal({ + title: `Select query from "${title}"`, + body: (props) => , + }); + } + + const [target] = targets; + selectQuery(target); + }, + configure: (context) => { + // Will only be visible for the Command Extensions dashboard + if (context?.dashboard?.title !== 'Link Extensions (onClick)') { + return undefined; + } + + if (!isSupported(context)) { + return; + } + + switch (context?.pluginId) { + case 'timeseries': + return {}; // Does not apply any overrides + case 'piechart': + return { + title: `Open from ${context.pluginId}`, + }; + + default: + // By returning undefined the extension will be hidden + return undefined; + } + }, + }); + +function isSupported(context?: PluginExtensionPanelContext): boolean { + const targets = context?.targets ?? []; + return targets.length > 0; +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e/test-plugins/grafana-extensionstest-app/package.json new file mode 100644 index 00000000000..0da20ae736b --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/package.json @@ -0,0 +1,48 @@ +{ + "name": "@test-plugins/extensions-test-app", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "webpack -c ./webpack.config.ts --env production", + "dev": "webpack -w -c ./webpack.config.ts --env development", + "typecheck": "tsc --noEmit", + "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx ." + }, + "author": "Grafana Labs", + "license": "Apache-2.0", + "devDependencies": { + "@grafana/eslint-config": "7.0.0", + "@grafana/plugin-configs": "11.3.0-pre", + "@types/lodash": "4.17.7", + "@types/node": "20.14.14", + "@types/prismjs": "1.26.4", + "@types/react": "18.3.3", + "@types/react-dom": "18.2.25", + "@types/semver": "7.5.8", + "@types/uuid": "9.0.8", + "glob": "10.4.1", + "ts-node": "10.9.2", + "typescript": "5.5.4", + "webpack": "5.91.0", + "webpack-merge": "5.10.0" + }, + "engines": { + "node": ">=20" + }, + "dependencies": { + "@emotion/css": "11.11.2", + "@grafana/data": "workspace:*", + "@grafana/runtime": "workspace:*", + "@grafana/schema": "workspace:*", + "@grafana/ui": "workspace:*", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-router-dom": "^6.22.0", + "rxjs": "7.8.1", + "tslib": "2.6.3" + }, + "peerDependencies": { + "@grafana/runtime": "*" + }, + "packageManager": "yarn@4.4.0" +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx new file mode 100644 index 00000000000..e15dc0a3482 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx @@ -0,0 +1,26 @@ +import { testIds } from '../components/testIds'; +import { PluginPage, usePluginComponents } from '@grafana/runtime'; +import { Stack } from '@grafana/ui'; + +type ReusableComponentProps = { + name: string; +}; + +export function AddedComponents() { + const { components } = usePluginComponents({ + extensionPointId: 'plugins/grafana-extensionexample2-app/addComponent/v1', + }); + + return ( + + +
+

Component extensions defined with addComponent and retrived with usePluginComponents hook

+ {components.map((Component, i) => { + return ; + })} +
+
+
+ ); +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx new file mode 100644 index 00000000000..d43b84a2da9 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx @@ -0,0 +1,24 @@ +import { testIds } from '../components/testIds'; +import { PluginPage, usePluginComponent } from '@grafana/runtime'; + +type ReusableComponentProps = { + name: string; +}; + +export function ExposedComponents() { + var { component: ReusableComponent } = usePluginComponent( + 'grafana-extensionexample1-app/reusable-component/v1' + ); + + if (!ReusableComponent) { + return null; + } + + return ( + +
+ +
+
+ ); +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyAPIs.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyAPIs.tsx new file mode 100644 index 00000000000..9407a2fca3f --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/pages/LegacyAPIs.tsx @@ -0,0 +1,44 @@ +import { testIds } from '../components/testIds'; +import { PluginPage, getPluginComponentExtensions, getPluginExtensions } from '@grafana/runtime'; +import { ActionButton } from '../components/ActionButton'; +import { Stack } from '@grafana/ui'; + +type AppExtensionContext = {}; +type ReusableComponentProps = { + name: string; +}; + +export function LegacyAPIs() { + const extensionPointId = 'plugins/grafana-extensionstest-app/actions'; + const context: AppExtensionContext = {}; + + const { extensions } = getPluginExtensions({ + extensionPointId, + context, + }); + + const { extensions: componentExtensions } = getPluginComponentExtensions({ + extensionPointId: 'plugins/grafana-extensionexample2-app/configure-extension-component/v1', + }); + + return ( + + +
+

Link extensions defined with configureExtensionLink and retrived using getPluginExtensions

+ +
+
+

+ Component extensions defined with configureExtensionComponent and retrived using + getPluginComponentExtensions +

+ {componentExtensions.map((extension) => { + const Component = extension.component; + return ; + })} +
+
+
+ ); +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx b/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx new file mode 100644 index 00000000000..2afacedc3be --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx @@ -0,0 +1,3 @@ +export { ExposedComponents } from './ExposedComponents'; +export { LegacyAPIs } from './LegacyAPIs'; +export { AddedComponents } from './AddedComponents'; diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugin.json b/e2e/test-plugins/grafana-extensionstest-app/plugin.json new file mode 100644 index 00000000000..aa6bae38085 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/plugin.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", + "type": "app", + "name": "Extensions test app", + "preload": true, + "id": "grafana-extensionstest-app", + "info": { + "keywords": ["app"], + "description": "", + "author": { + "name": "Grafana" + }, + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "screenshots": [], + "version": "%VERSION%", + "updated": "%TODAY%" + }, + "includes": [ + { + "type": "page", + "name": "Legacy APIs", + "path": "/a/grafana-extensionstest-app/legacy-apis", + "role": "Admin", + "addToNav": true, + "defaultNav": false + }, + { + "type": "page", + "name": "Exposed components", + "path": "/a/grafana-extensionstest-app/exposed-components", + "role": "Admin", + "addToNav": true, + "defaultNav": false + }, + { + "type": "page", + "name": "Added components", + "path": "/a/grafana-extensionstest-app/added-components", + "role": "Admin", + "addToNav": true, + "defaultNav": false + }, + { + "type": "page", + "icon": "cog", + "name": "Configuration", + "path": "/plugins/grafana-extensionstest-app", + "role": "Admin", + "addToNav": true + } + ], + "dependencies": { + "grafanaDependency": ">=10.4.0", + "plugins": [] + } +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx new file mode 100644 index 00000000000..b5cbe8713b7 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { AppRootProps } from '@grafana/data'; +import { testIds } from '../../testIds'; + +export class App extends React.PureComponent { + render() { + return ( +
+ Hello Grafana! +
+ ); + } +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx new file mode 100644 index 00000000000..ac7ba3b3a24 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx @@ -0,0 +1 @@ +export * from './App'; diff --git a/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/img/logo.svg b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg similarity index 100% rename from e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/img/logo.svg rename to e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx new file mode 100644 index 00000000000..602ad153afb --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx @@ -0,0 +1,17 @@ +import { AppPlugin } from '@grafana/data'; +import { App } from './components/App'; + +export const plugin = new AppPlugin<{}>() + .setRootPage(App) + .configureExtensionLink({ + title: 'Go to A', + description: 'Navigating to pluging A', + extensionPointId: 'plugins/grafana-extensionstest-app/actions', + path: '/a/grafana-extensionexample1-app/', + }) + .exposeComponent({ + id: 'grafana-extensionexample1-app/reusable-component/v1', + title: 'Reusable component', + description: 'A component that can be reused by other app plugins.', + component: ({ name }: { name: string }) =>
Hello {name}!
, + }); diff --git a/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/plugin.json b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json similarity index 86% rename from e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/plugin.json rename to e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json index 3487cd45540..3d012351bbe 100644 --- a/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/plugin.json +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", "type": "app", - "name": "A App", - "id": "myorg-componentexposer-app", + "name": "C App", + "id": "grafana-extensionexample1-app", "preload": true, "info": { "keywords": ["app"], @@ -22,7 +22,7 @@ { "type": "page", "name": "Default", - "path": "/a/myorg-componentexposer-app", + "path": "/a/grafana-extensionexample1-app", "role": "Admin", "addToNav": false, "defaultNav": false diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/testIds.ts b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/testIds.ts new file mode 100644 index 00000000000..5bce9218a25 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/testIds.ts @@ -0,0 +1,16 @@ +export const testIds = { + container: 'main-app-body', + actions: { + button: 'action-button', + }, + modal: { + container: 'container', + open: 'open-link', + }, + appA: { + container: 'a-app-body', + }, + appB: { + modal: 'b-app-modal', + }, +}; diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx new file mode 100644 index 00000000000..3c018a9c0f2 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { AppRootProps } from '@grafana/data'; + +export class App extends React.PureComponent { + render() { + return
Hello Grafana!
; + } +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx new file mode 100644 index 00000000000..ac7ba3b3a24 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx @@ -0,0 +1 @@ +export * from './App'; diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg new file mode 100644 index 00000000000..3d284dea3af --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx new file mode 100644 index 00000000000..6a5e0d3a0b9 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx @@ -0,0 +1,32 @@ +import { AppPlugin } from '@grafana/data'; +import { App } from './components/App'; +import { testIds } from './testIds'; + +console.log('Hello from app B'); +export const plugin = new AppPlugin<{}>() + .setRootPage(App) + .configureExtensionLink({ + title: 'Open from B', + description: 'Open a modal from plugin B', + extensionPointId: 'plugins/grafana-extensionstest-app/actions', + onClick: (_, { openModal }) => { + openModal({ + title: 'Modal from app B', + body: () =>
From plugin B
, + }); + }, + }) + .configureExtensionComponent({ + extensionPointId: 'plugins/grafana-extensionexample2-app/configure-extension-component/v1', + title: 'Configure extension component from B', + description: 'A component that can be reused by other app plugins. Shared using configureExtensionComponent api', + component: ({ name }: { name: string }) =>
Hello {name}!
, + }) + .addComponent<{ name: string }>({ + targets: 'plugins/grafana-extensionexample2-app/addComponent/v1', + title: 'Added component from B', + description: 'A component that can be reused by other app plugins. Shared using addComponent api', + component: ({ name }: { name: string }) => ( +
Hello {name}!
+ ), + }); diff --git a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json similarity index 68% rename from e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json rename to e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json index 7bd55c0e572..f27f9e1113b 100644 --- a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json @@ -1,14 +1,14 @@ { "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", "type": "app", - "name": "B App", - "id": "myorg-b-app", + "name": "D App", + "id": "grafana-extensionexample2-app", "preload": true, "info": { "keywords": ["app"], "description": "Will extend root app with ui extensions", "author": { - "name": "Myorg" + "name": "grafana" }, "logos": { "small": "img/logo.svg", @@ -22,7 +22,7 @@ { "type": "page", "name": "Default", - "path": "/a/myorg-b-app", + "path": "/a/grafana-extensionexample2-app", "role": "Admin", "addToNav": false, "defaultNav": false @@ -31,13 +31,5 @@ "dependencies": { "grafanaDependency": ">=10.3.3", "plugins": [] - }, - "extensions": [ - { - "extensionPointId": "plugins/myorg-extensionpoint-app/actions", - "title": "Open from B", - "description": "Open a modal from plugin B", - "type": "link" - } - ] + } } diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/testIds.ts b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/testIds.ts new file mode 100644 index 00000000000..240a7710d7a --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/testIds.ts @@ -0,0 +1,18 @@ +export const testIds = { + container: 'main-app-body', + actions: { + button: 'action-button', + }, + modal: { + container: 'container', + open: 'open-link', + }, + appA: { + container: 'a-app-body', + }, + appB: { + modal: 'b-app-modal', + reusableComponent: 'b-app-configure-extension-component', + reusableAddedComponent: 'b-app-add-component', + }, +}; diff --git a/e2e/test-plugins/grafana-extensionstest-app/tsconfig.json b/e2e/test-plugins/grafana-extensionstest-app/tsconfig.json new file mode 100644 index 00000000000..40352099203 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "types": ["node", "jest", "@testing-library/jest-dom"] + }, + "extends": "@grafana/plugin-configs/tsconfig.json", + "include": ["."] +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts b/e2e/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts new file mode 100644 index 00000000000..b9e4c9926bb --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts @@ -0,0 +1,6 @@ +import { PLUGIN_BASE_URL } from '../constants'; + +// Prefixes the route with the base URL of the plugin +export function prefixRoute(route: string): string { + return `${PLUGIN_BASE_URL}/${route}`; +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/utils/utils.ts b/e2e/test-plugins/grafana-extensionstest-app/utils/utils.ts new file mode 100644 index 00000000000..5244f2afbb0 --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/utils/utils.ts @@ -0,0 +1,5 @@ +import { DataQuery } from '@grafana/data'; + +export function selectQuery(target: DataQuery): void { + alert(`You selected query "${target.refId}"`); +} diff --git a/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts b/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts new file mode 100644 index 00000000000..3303ed94f3c --- /dev/null +++ b/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts @@ -0,0 +1,44 @@ +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import grafanaConfig from '@grafana/plugin-configs/webpack.config'; +import { mergeWithCustomize, unique } from 'webpack-merge'; +import { Configuration } from 'webpack'; + +function skipFiles(f: string): boolean { + if (f.includes('/dist/')) { + // avoid copying files already in dist + return false; + } + if (f.includes('/node_modules/')) { + // avoid copying tsconfig.json + return false; + } + if (f.includes('/package.json')) { + // avoid copying package.json + return false; + } + return true; +} + +const config = async (env: Record): Promise => { + const baseConfig = await grafanaConfig(env); + const customConfig = { + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + // To `compiler.options.output` + { from: 'README.md', to: '.', force: true }, + { from: 'plugin.json', to: '.' }, + { from: 'CHANGELOG.md', to: '.', force: true }, + { from: '**/*.json', to: '.', filter: skipFiles }, + { from: '**/*.svg', to: '.', noErrorOnMissing: true, filter: skipFiles }, // Optional + ], + }), + ], + }; + + return mergeWithCustomize({ + customizeArray: unique('plugins', ['CopyPlugin'], (plugin) => plugin.constructor && plugin.constructor.name), + })(baseConfig, customConfig); +}; + +export default config; diff --git a/package.json b/package.json index 9542ceff165..257050cda42 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "e2e:playwright": "yarn playwright test", "e2e:playwright:server": "./e2e/plugin-e2e/start-and-run-suite", "e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true", + "e2e:plugin:build": "nx run-many -t build --projects='@test-plugins/*'", + "e2e:plugin:build:dev": "nx run-many -t dev --projects='@test-plugins/*' --maxParallel=100", "test": "jest --notify --watch", "test:coverage": "jest --coverage", "test:coverage:changes": "jest --coverage --changedSince=origin/main", @@ -431,7 +433,8 @@ "packages/*", "packages/!(grafana-icons)/**", "plugins-bundled/internal/*", - "public/app/plugins/*/*" + "public/app/plugins/*/*", + "e2e/test-plugins/*" ] }, "engines": { diff --git a/packages/grafana-plugin-configs/utils.ts b/packages/grafana-plugin-configs/utils.ts index a8b3dbd113c..3a74dd90af6 100644 --- a/packages/grafana-plugin-configs/utils.ts +++ b/packages/grafana-plugin-configs/utils.ts @@ -12,13 +12,29 @@ export function getPluginJson() { } export async function getEntries(): Promise> { - const pluginModules = await glob(path.resolve(process.cwd(), `module.{ts,tsx}`), { absolute: true }); - if (pluginModules.length > 0) { - return { - module: pluginModules[0], - }; - } - throw new Error('Could not find module.ts or module.tsx file'); + const pluginsJson = await glob(path.resolve(process.cwd(), '**/plugin.json'), { + ignore: ['**/dist/**'], + absolute: true, + }); + + const plugins = await Promise.all( + pluginsJson.map((pluginJson) => { + const folder = path.dirname(pluginJson); + return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true }); + }) + ); + + let result: Record = {}; + return plugins.reduce((result, modules) => { + return modules.reduce((result, module) => { + const pluginPath = path.dirname(module); + const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, ''); + const entryName = pluginName === '' ? 'module' : `${pluginName}/module`; + + result[entryName] = module; + return result; + }, result); + }, result); } export function hasLicense() { diff --git a/packages/grafana-plugin-configs/webpack.config.ts b/packages/grafana-plugin-configs/webpack.config.ts index e3b5bd097bb..f2c0a382d0c 100644 --- a/packages/grafana-plugin-configs/webpack.config.ts +++ b/packages/grafana-plugin-configs/webpack.config.ts @@ -59,7 +59,6 @@ const config = async (env: Record): Promise => { 'redux', 'rxjs', 'react-router', - 'react-router-dom', 'd3', 'angular', '@grafana/ui', diff --git a/scripts/drone/pipelines/build.star b/scripts/drone/pipelines/build.star index c28a734330a..722e2ca1136 100644 --- a/scripts/drone/pipelines/build.star +++ b/scripts/drone/pipelines/build.star @@ -4,6 +4,7 @@ load( "scripts/drone/steps/lib.star", "build_frontend_package_step", "build_storybook_step", + "build_test_plugins_step", "cloud_plugins_e2e_tests_step", "compile_build_cmd", "download_grabpl_step", @@ -93,6 +94,7 @@ def build_e2e(trigger, ver_mode): build_steps.extend( [ + build_test_plugins_step(), grafana_server_step(), e2e_tests_step("dashboards-suite"), e2e_tests_step("scenes/dashboards-suite"), diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index e07ae798698..80756682c3b 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -470,6 +470,26 @@ def build_frontend_step(): ], } +def build_test_plugins_step(): + """Build the test plugins used in e2e tests + + Returns: + Drone step. + """ + return { + "name": "build-test-plugins", + "image": images["node"], + "environment": { + "NODE_OPTIONS": "--max_old_space_size=8192", + }, + "depends_on": [ + "yarn-install", + ], + "commands": [ + "yarn e2e:plugin:build", + ], + } + def update_package_json_version(): """Updates the packages/ to use a version that has the build ID in it: 10.0.0pre -> 10.0.0-5432pre @@ -773,6 +793,7 @@ def e2e_tests_step(suite, port = 3001, tries = None): "image": images["cypress"], "depends_on": [ "grafana-server", + "build-test-plugins", ], "environment": { "HOST": "grafana-server", @@ -872,6 +893,7 @@ def playwright_e2e_tests_step(): "image": images["node_deb"], "depends_on": [ "grafana-server", + "build-test-plugins", ], "commands": [ "npx wait-on@7.0.1 http://$HOST:$PORT", diff --git a/scripts/grafana-server/custom.ini b/scripts/grafana-server/custom.ini index 8a45c8ed2fb..5d09d40ac63 100644 --- a/scripts/grafana-server/custom.ini +++ b/scripts/grafana-server/custom.ini @@ -1,3 +1,4 @@ + [security] content_security_policy = true content_security_policy_template = """require-trusted-types-for 'script'; script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';""" @@ -5,6 +6,9 @@ content_security_policy_template = """require-trusted-types-for 'script'; script [feature_toggles] enable = publicDashboards +[plugins] +allow_loading_unsigned_plugins=grafana-extensionstest-app,grafana-extensionexample1-app,grafana-extensionexample2-app, + [database] type=sqlite3 wal=true diff --git a/scripts/grafana-server/start-server b/scripts/grafana-server/start-server index 83e74b7508a..a4e9be7f2bd 100755 --- a/scripts/grafana-server/start-server +++ b/scripts/grafana-server/start-server @@ -23,8 +23,9 @@ fi echo starting server cp -r ./bin $RUNDIR -cp -r ./public $RUNDIR cp -r ./tools $RUNDIR +ln -s $(realpath ./public) $RUNDIR + mkdir $RUNDIR/conf mkdir $PROV_DIR @@ -39,12 +40,12 @@ cp ./conf/defaults.ini $RUNDIR/conf/defaults.ini echo -e "Copying custom plugins from e2e tests" mkdir -p "$RUNDIR/data/plugins" -# when running in a local computer -if [ -d "./e2e/custom-plugins" ]; then - cp -r "./e2e/custom-plugins" "$RUNDIR/data/plugins" + +if [ -d "./e2e/test-plugins" ]; then + ln -s $(realpath ./e2e/test-plugins/*) "$RUNDIR/data/plugins" # when running in CI -elif [ -d "../e2e/custom-plugins" ]; then - cp -r "../e2e/custom-plugins" "$RUNDIR/data/plugins" +elif [ -d "../e2e/test-plugins" ]; then + cp -r "../e2e/test-plugins" "$RUNDIR/data/plugins" fi echo -e "Copy provisioning setup from devenv" diff --git a/yarn.lock b/yarn.lock index 03c3dd62c34..38872ab3bb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6678,6 +6678,13 @@ __metadata: languageName: node linkType: hard +"@remix-run/router@npm:1.19.1": + version: 1.19.1 + resolution: "@remix-run/router@npm:1.19.1" + checksum: 10/2800c2f6567a982fe942aacc4cb5b170e7cc89bd455960e3bea2424161ff7dac32d01886322d88dd19b88d1bea711f39566d17f02b73eeb74999affb471f8f52 + languageName: node + linkType: hard + "@rollup/plugin-image@npm:3.0.3": version: 3.0.3 resolution: "@rollup/plugin-image@npm:3.0.3" @@ -8796,6 +8803,39 @@ __metadata: languageName: node linkType: hard +"@test-plugins/extensions-test-app@workspace:e2e/test-plugins/grafana-extensionstest-app": + version: 0.0.0-use.local + resolution: "@test-plugins/extensions-test-app@workspace:e2e/test-plugins/grafana-extensionstest-app" + dependencies: + "@emotion/css": "npm:11.11.2" + "@grafana/data": "workspace:*" + "@grafana/eslint-config": "npm:7.0.0" + "@grafana/plugin-configs": "npm:11.3.0-pre" + "@grafana/runtime": "workspace:*" + "@grafana/schema": "workspace:*" + "@grafana/ui": "workspace:*" + "@types/lodash": "npm:4.17.7" + "@types/node": "npm:20.14.14" + "@types/prismjs": "npm:1.26.4" + "@types/react": "npm:18.3.3" + "@types/react-dom": "npm:18.2.25" + "@types/semver": "npm:7.5.8" + "@types/uuid": "npm:9.0.8" + glob: "npm:10.4.1" + react: "npm:18.2.0" + react-dom: "npm:18.2.0" + react-router-dom: "npm:^6.22.0" + rxjs: "npm:7.8.1" + ts-node: "npm:10.9.2" + tslib: "npm:2.6.3" + typescript: "npm:5.5.4" + webpack: "npm:5.91.0" + webpack-merge: "npm:5.10.0" + peerDependencies: + "@grafana/runtime": "*" + languageName: unknown + linkType: soft + "@testing-library/dom@npm:10.0.0, @testing-library/dom@npm:>=7, @testing-library/dom@npm:^10.0.0": version: 10.0.0 resolution: "@testing-library/dom@npm:10.0.0" @@ -27286,6 +27326,19 @@ __metadata: languageName: node linkType: hard +"react-router-dom@npm:^6.22.0": + version: 6.26.1 + resolution: "react-router-dom@npm:6.26.1" + dependencies: + "@remix-run/router": "npm:1.19.1" + react-router: "npm:6.26.1" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 10/1bd255d1ff88f477699c72656e7c07702a907e644388a1bea1c648f2df0c3c86db2e90bea945b1d43eaf84ebab194f3868f3788502965ad5f20c508c6874f1fe + languageName: node + linkType: hard + "react-router@npm:5.3.3": version: 5.3.3 resolution: "react-router@npm:5.3.3" @@ -27317,6 +27370,17 @@ __metadata: languageName: node linkType: hard +"react-router@npm:6.26.1": + version: 6.26.1 + resolution: "react-router@npm:6.26.1" + dependencies: + "@remix-run/router": "npm:1.19.1" + peerDependencies: + react: ">=16.8" + checksum: 10/b3761515c75da65a1678f005d08a6285ceccd9df7237ae6fdd9ab2ab816ef328435b75610f705ecd9ecd41c6878fd22eb9b44c5391cdef2e1ed99ddbc78de8a4 + languageName: node + linkType: hard + "react-select-event@npm:5.5.1, react-select-event@npm:^5.1.0": version: 5.5.1 resolution: "react-select-event@npm:5.5.1"