diff --git a/devenv/plugins.yaml b/devenv/plugins.yaml
index 9e488cc065f..097428a6ca9 100644
--- a/devenv/plugins.yaml
+++ b/devenv/plugins.yaml
@@ -17,3 +17,11 @@ apps:
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
+ org_id: 1
+ org_name: Main Org.
+ disabled: false
diff --git a/e2e/custom-plugins/app-with-exposed-components/README.md b/e2e/custom-plugins/app-with-exposed-components/README.md
new file mode 100644
index 00000000000..03590601496
--- /dev/null
+++ b/e2e/custom-plugins/app-with-exposed-components/README.md
@@ -0,0 +1,22 @@
+# 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/img/logo.svg b/e2e/custom-plugins/app-with-exposed-components/img/logo.svg
new file mode 100644
index 00000000000..3d284dea3af
--- /dev/null
+++ b/e2e/custom-plugins/app-with-exposed-components/img/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e2e/custom-plugins/app-with-exposed-components/module.js b/e2e/custom-plugins/app-with-exposed-components/module.js
new file mode 100644
index 00000000000..b73fbd617ce
--- /dev/null
+++ b/e2e/custom-plugins/app-with-exposed-components/module.js
@@ -0,0 +1,28 @@
+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
new file mode 100644
index 00000000000..caf74b2d1a8
--- /dev/null
+++ b/e2e/custom-plugins/app-with-exposed-components/plugin.json
@@ -0,0 +1,35 @@
+{
+ "$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/img/logo.svg b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/img/logo.svg
new file mode 100644
index 00000000000..3d284dea3af
--- /dev/null
+++ b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/img/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
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
new file mode 100644
index 00000000000..4ccaba8ca36
--- /dev/null
+++ b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/module.js
@@ -0,0 +1,14 @@
+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-exposed-components/plugins/myorg-componentexposer-app/plugin.json b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/plugin.json
new file mode 100644
index 00000000000..3487cd45540
--- /dev/null
+++ b/e2e/custom-plugins/app-with-exposed-components/plugins/myorg-componentexposer-app/plugin.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
+ "type": "app",
+ "name": "A App",
+ "id": "myorg-componentexposer-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-componentexposer-app",
+ "role": "Admin",
+ "addToNav": false,
+ "defaultNav": false
+ }
+ ],
+ "dependencies": {
+ "grafanaDependency": ">=10.3.3",
+ "plugins": []
+ }
+}
diff --git a/e2e/custom-plugins/app-with-extension-point/plugin.json b/e2e/custom-plugins/app-with-extension-point/plugin.json
index b1d3c396384..3a4ab08faa2 100644
--- a/e2e/custom-plugins/app-with-extension-point/plugin.json
+++ b/e2e/custom-plugins/app-with-extension-point/plugin.json
@@ -32,7 +32,5 @@
"grafanaDependency": ">=10.3.3",
"plugins": []
},
- "generated": {
- "extensions": []
- }
+ "extensions": []
}
diff --git a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json
index ac501bc7f56..7bd55c0e572 100644
--- a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json
+++ b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json
@@ -32,14 +32,12 @@
"grafanaDependency": ">=10.3.3",
"plugins": []
},
- "generated": {
- "extensions": [
- {
- "extensionPointId": "plugins/myorg-extensionpoint-app/actions",
- "title": "Open from B",
- "description": "Open a modal from plugin B",
- "type": "link"
- }
- ]
- }
+ "extensions": [
+ {
+ "extensionPointId": "plugins/myorg-extensionpoint-app/actions",
+ "title": "Open from B",
+ "description": "Open a modal from plugin B",
+ "type": "link"
+ }
+ ]
}
diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensionPoints.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts
similarity index 100%
rename from e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensionPoints.spec.ts
rename to e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensionPoints.spec.ts
diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts
similarity index 100%
rename from e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions.spec.ts
rename to e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/extensions.spec.ts
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
new file mode 100644
index 00000000000..9a01139b445
--- /dev/null
+++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions/useExposedComponent.spec.ts
@@ -0,0 +1,9 @@
+import { test, expect } from '@grafana/plugin-e2e';
+
+const pluginId = 'myorg-componentconsumer-app';
+const exposedComponentTestId = 'exposed-component';
+
+test('should display component exposed by another app', async ({ page }) => {
+ await page.goto(`/a/${pluginId}`);
+ await expect(await page.getByTestId(exposedComponentTestId)).toHaveText('Hello World!');
+});