From 1b278cc87e0a782304b330935c7c3838651bde8d Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 6 Nov 2025 11:06:53 +0100 Subject: [PATCH] Plugins: cleanup feature toggle pluginsFrontendSandbox (#113439) * clean feature pluginsFrontendSandbox Co-authored-by: Andres Martinez Gotor --- conf/defaults.ini | 1 - conf/sample.ini | 1 - .../plugin-frontend-sandbox.md | 6 +- .../frontend-sandbox-panel.spec.ts | 157 +++------ .../frontend-sandbox-app.spec.ts | 60 +--- .../frontend-sandbox-datasource.spec.ts | 305 ++++++------------ .../src/types/featureToggles.gen.ts | 4 - pkg/services/featuremgmt/registry.go | 6 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 3 +- .../sandboxPluginLoaderRegistry.test.ts | 8 - .../sandbox/sandboxPluginLoaderRegistry.ts | 5 - 13 files changed, 152 insertions(+), 409 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 61d0ca7e318..5373b53882c 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -443,7 +443,6 @@ content_security_policy_report_only_template = """script-src 'self' 'unsafe-eval csrf_always_check = false # Comma-separated list of plugins ids that will be loaded inside the frontend sandbox -# Currently behind the feature flag pluginsFrontendSandbox enable_frontend_sandbox_for_plugins = # Comma-separated list of paths for POST/PUT URL in actions. Empty will allow anything that is not on the same origin diff --git a/conf/sample.ini b/conf/sample.ini index bee580aaa98..78a77294a99 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -442,7 +442,6 @@ ;csrf_always_check = false # Comma-separated list of plugins ids that will be loaded inside the frontend sandbox -# Currently behind the feature flag pluginsFrontendSandbox ;enable_frontend_sandbox_for_plugins = # Comma-separated list of paths for POST/PUT URL in actions. Empty will allow anything that is not on the same origin diff --git a/docs/sources/administration/plugin-management/plugin-frontend-sandbox.md b/docs/sources/administration/plugin-management/plugin-frontend-sandbox.md index 19c5c6f72b3..1b2721d49a8 100644 --- a/docs/sources/administration/plugin-management/plugin-frontend-sandbox.md +++ b/docs/sources/administration/plugin-management/plugin-frontend-sandbox.md @@ -53,11 +53,7 @@ The following applies: ## Enable the Frontend Sandbox -The Frontend Sandbox feature is currently behind the `pluginsFrontendSandbox` feature flag. To enable it, you need to: - -1. Enable the feature flag in your Grafana configuration. For more information about enabling feature flags, refer to [Configure feature toggles](/docs/grafana//setup-grafana/configure-grafana/feature-toggles/). - -2. For self-hosted Grafana installations, add the plugin IDs you want to sandbox in the `security` section using the `enable_frontend_sandbox_for_plugins` configuration option. +For self-hosted Grafana installations, add the plugin IDs you want to sandbox in the `security` section using the `enable_frontend_sandbox_for_plugins` configuration option. For Grafana Cloud users, you can simply use the toggle switch in the plugin catalog page to enable or disable the sandbox for each plugin. By default, the sandbox is disabled for all plugins. diff --git a/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts b/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts index 4cdb7afb081..b9100201f02 100644 --- a/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts +++ b/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts @@ -22,133 +22,64 @@ test.describe( tag: ['@panels'], }, () => { - test.describe('Sandbox disabled', () => { - test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); - }); - await page.reload(); + test('Does not add iframes to body', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, }); - test('Add iframes to body', async ({ page, gotoDashboardPage }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - }); + // this button adds 3 iframes to the body + await page.locator('[data-testid="button-create-iframes"]').click(); - // this button adds iframes to the body - await page.locator('[data-testid="button-create-iframes"]').click(); + const iframeIds = [ + 'createElementIframe', + 'innerHTMLIframe', + 'appendIframe', + 'prependIframe', + 'afterIframe', + 'beforeIframe', + 'outerHTMLIframe', + 'parseFromStringIframe', + 'insertBeforeIframe', + 'replaceChildIframe', + ]; - const iframeIds = [ - 'createElementIframe', - 'innerHTMLIframe', - 'appendIframe', - 'prependIframe', - 'afterIframe', - 'beforeIframe', - 'outerHTMLIframe', - 'parseFromStringIframe', - 'insertBeforeIframe', - 'replaceChildIframe', - ]; - - for (const id of iframeIds) { - await expect(page.locator(`#${id}`)).toBeVisible(); - } - }); - - test('Reaches out of panel div', async ({ page, gotoDashboardPage }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - }); - - // this button reaches out of the panel div and modifies the element dataset - await page.locator('[data-testid="button-reach-out"]').click(); - await expect(page.locator('[data-sandbox-test="true"]')).toBeVisible(); - }); - - test('Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - queryParams: new URLSearchParams({ editPanel: '1' }), - }); - - const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); - await expect(input).toBeEnabled(); - await expect(input).toHaveValue(''); - - await input.fill('x'); - await expect(input).toHaveValue('x'); - await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeVisible(); - }); + for (const id of iframeIds) { + await expect(page.locator(`#${id}`)).toBeHidden(); + } }); - test.describe('Sandbox enabled', () => { - test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); - }); - await page.reload(); + test('Does not reaches out of panel div', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, }); - test('Does not add iframes to body', async ({ page, gotoDashboardPage }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - }); + // this button reaches out of the panel div and modifies the element dataset + await page.locator('[data-testid="button-reach-out"]').click(); + await expect(page.locator('[data-sandbox-test="true"]')).toBeHidden(); + }); - // this button adds 3 iframes to the body - await page.locator('[data-testid="button-create-iframes"]').click(); - - const iframeIds = [ - 'createElementIframe', - 'innerHTMLIframe', - 'appendIframe', - 'prependIframe', - 'afterIframe', - 'beforeIframe', - 'outerHTMLIframe', - 'parseFromStringIframe', - 'insertBeforeIframe', - 'replaceChildIframe', - ]; - - for (const id of iframeIds) { - await expect(page.locator(`#${id}`)).toBeHidden(); - } + test('Does not Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), }); - test('Does not reaches out of panel div', async ({ page, gotoDashboardPage }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - }); + const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); + await expect(input).toBeEnabled(); - // this button reaches out of the panel div and modifies the element dataset - await page.locator('[data-testid="button-reach-out"]').click(); - await expect(page.locator('[data-sandbox-test="true"]')).toBeHidden(); + await input.fill('x'); + await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeHidden(); + }); + + test('Can access specific window global variables', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, }); - test('Does not Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - queryParams: new URLSearchParams({ editPanel: '1' }), - }); - - const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); - await expect(input).toBeEnabled(); - - await input.fill('x'); - await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeHidden(); - }); - - test('Can access specific window global variables', async ({ page, gotoDashboardPage }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - }); - - await page.locator('[data-testid="button-test-globals"]').click(); - await expect(page.locator('[data-sandbox-global="Prism"]')).toBeVisible(); - await expect(page.locator('[data-sandbox-global="jQuery"]')).toBeVisible(); - await expect(page.locator('[data-sandbox-global="location"]')).toBeVisible(); - }); + await page.locator('[data-testid="button-test-globals"]').click(); + await expect(page.locator('[data-sandbox-global="Prism"]')).toBeVisible(); + await expect(page.locator('[data-sandbox-global="jQuery"]')).toBeVisible(); + await expect(page.locator('[data-sandbox-global="location"]')).toBeVisible(); }); } ); diff --git a/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts index dfa19753b15..15fcdb8016c 100644 --- a/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts +++ b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts @@ -17,60 +17,24 @@ test.describe( }); test.describe('App Page', () => { - test.describe('Sandbox disabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); - }); - }); + test('Loads the app page with the sandbox div wrapper', async ({ page }) => { + await page.goto(`/a/${APP_ID}`); - test('Loads the app page without the sandbox div wrapper', async ({ page }) => { - await page.goto(`/a/${APP_ID}`); + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeVisible(); - const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); - await expect(sandboxDiv).toBeHidden(); - - const appPage = page.getByTestId('sandbox-app-test-page-one'); - await expect(appPage).toBeVisible(); - }); - - test('Loads the app configuration without the sandbox div wrapper', async ({ page }) => { - await page.goto(`/plugins/${APP_ID}`); - - const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); - await expect(sandboxDiv).toBeHidden(); - - const configPage = page.getByTestId('sandbox-app-test-config-page'); - await expect(configPage).toBeVisible(); - }); + const appPage = page.getByTestId('sandbox-app-test-page-one'); + await expect(appPage).toBeVisible(); }); - test.describe('Sandbox enabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); - }); - }); + test('Loads the app configuration with the sandbox div wrapper', async ({ page }) => { + await page.goto(`/plugins/${APP_ID}`); - test('Loads the app page with the sandbox div wrapper', async ({ page }) => { - await page.goto(`/a/${APP_ID}`); + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeVisible(); - const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); - await expect(sandboxDiv).toBeVisible(); - - const appPage = page.getByTestId('sandbox-app-test-page-one'); - await expect(appPage).toBeVisible(); - }); - - test('Loads the app configuration with the sandbox div wrapper', async ({ page }) => { - await page.goto(`/plugins/${APP_ID}`); - - const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); - await expect(sandboxDiv).toBeVisible(); - - const configPage = page.getByTestId('sandbox-app-test-config-page'); - await expect(configPage).toBeVisible(); - }); + const configPage = page.getByTestId('sandbox-app-test-config-page'); + await expect(configPage).toBeVisible(); }); }); } diff --git a/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts index acddc23018c..339d3c24c19 100644 --- a/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts +++ b/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts @@ -11,246 +11,127 @@ test.describe( }, () => { test.describe('Config Editor', () => { - test.describe('Sandbox disabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); - }); + test('Should render a sandbox wrapper around the datasource config editor', async ({ + page, + createDataSource, + }) => { + const TIMESTAMP = Date.now(); + const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; + // Add the datasource + const response = await createDataSource({ + type: DATASOURCE_ID, + name: DATASOURCE_TYPED_NAME, }); + const DATASOURCE_CONNECTION_ID = response.uid; + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); - test('Should not render a sandbox wrapper around the datasource config editor', async ({ - page, - createDataSource, - }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); - - const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); - await expect(sandboxDiv).toBeHidden(); - }); + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeVisible(); }); - test.describe('Sandbox enabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); - }); + test('Should store values in jsonData and secureJsonData correctly', async ({ page, createDataSource }) => { + const TIMESTAMP = Date.now(); + const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; + // Add the datasource + const response = await createDataSource({ + type: DATASOURCE_ID, + name: DATASOURCE_TYPED_NAME, }); + const DATASOURCE_CONNECTION_ID = response.uid; + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); - test('Should render a sandbox wrapper around the datasource config editor', async ({ - page, - createDataSource, - }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + const valueToStore = 'test' + random(100); - const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); - await expect(sandboxDiv).toBeVisible(); - }); + const queryInput = page.locator('[data-testid="sandbox-config-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToStore); + await expect(queryInput).toHaveValue(valueToStore); - test('Should store values in jsonData and secureJsonData correctly', async ({ page, createDataSource }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + const saveButton = page.getByTestId('data-testid Data source settings page Save and Test button'); + await saveButton.click(); - const valueToStore = 'test' + random(100); + const alert = page.getByTestId('data-testid Data source settings page Alert'); + await expect(alert).toBeVisible(); + await expect(alert).toContainText('Sandbox Success'); - const queryInput = page.locator('[data-testid="sandbox-config-editor-query-input"]'); - await expect(queryInput).not.toBeDisabled(); - await queryInput.fill(valueToStore); - await expect(queryInput).toHaveValue(valueToStore); - - const saveButton = page.getByTestId('data-testid Data source settings page Save and Test button'); - await saveButton.click(); - - const alert = page.getByTestId('data-testid Data source settings page Alert'); - await expect(alert).toBeVisible(); - await expect(alert).toContainText('Sandbox Success'); - - // validate the value was stored - await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); - await expect(queryInput).not.toBeDisabled(); - await expect(queryInput).toHaveValue(valueToStore); - }); + // validate the value was stored + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + await expect(queryInput).not.toBeDisabled(); + await expect(queryInput).toHaveValue(valueToStore); }); }); test.describe('Explore Page', () => { - test.describe('Sandbox disabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); - }); + test('Should wrap the query editor in a sandbox wrapper', async ({ + page, + createDataSource, + dashboardPage, + selectors, + }) => { + const TIMESTAMP = Date.now(); + const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; + // Add the datasource + const response = await createDataSource({ + type: DATASOURCE_ID, + name: DATASOURCE_TYPED_NAME, }); + const DATASOURCE_CONNECTION_ID = response.uid; + await page.goto('/explore'); - test('Should not wrap the query editor in a sandbox wrapper', async ({ - page, - createDataSource, - dashboardPage, - selectors, - }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto('/explore'); + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); - const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); - await expect(dataSourcePicker).toBeVisible(); - await dataSourcePicker.click(); + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); - const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); - await expect(datasourceOption).toBeVisible(); - await datasourceOption.scrollIntoViewIfNeeded(); - await datasourceOption.click(); + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); - // make sure the datasource was correctly selected and rendered - const breadcrumb = dashboardPage.getByGrafanaSelector( - selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) - ); - await expect(breadcrumb).toBeVisible(); - - const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); - await expect(sandboxDiv).toBeHidden(); - }); - - test('Should accept values when typed', async ({ page, createDataSource, dashboardPage, selectors }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto('/explore'); - - const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); - await expect(dataSourcePicker).toBeVisible(); - await dataSourcePicker.click(); - - const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); - await expect(datasourceOption).toBeVisible(); - await datasourceOption.scrollIntoViewIfNeeded(); - await datasourceOption.click(); - - // make sure the datasource was correctly selected and rendered - const breadcrumb = dashboardPage.getByGrafanaSelector( - selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) - ); - await expect(breadcrumb).toBeVisible(); - - const valueToType = 'test' + random(100); - - const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); - await expect(queryInput).not.toBeDisabled(); - await queryInput.fill(valueToType); - await expect(queryInput).toHaveValue(valueToType); - }); + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeVisible(); }); - test.describe('Sandbox enabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); - }); + test('Should accept values when typed', async ({ page, createDataSource, dashboardPage, selectors }) => { + const TIMESTAMP = Date.now(); + const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; + // Add the datasource + const response = await createDataSource({ + type: DATASOURCE_ID, + name: DATASOURCE_TYPED_NAME, }); + const DATASOURCE_CONNECTION_ID = response.uid; + await page.goto('/explore'); - test('Should wrap the query editor in a sandbox wrapper', async ({ - page, - createDataSource, - dashboardPage, - selectors, - }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto('/explore'); + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); - const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); - await expect(dataSourcePicker).toBeVisible(); - await dataSourcePicker.click(); + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); - const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); - await expect(datasourceOption).toBeVisible(); - await datasourceOption.scrollIntoViewIfNeeded(); - await datasourceOption.click(); + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); - // make sure the datasource was correctly selected and rendered - const breadcrumb = dashboardPage.getByGrafanaSelector( - selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) - ); - await expect(breadcrumb).toBeVisible(); + const valueToType = 'test' + random(100); - const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); - await expect(sandboxDiv).toBeVisible(); - }); + const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToType); + await expect(queryInput).toHaveValue(valueToType); - test('Should accept values when typed', async ({ page, createDataSource, dashboardPage, selectors }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto('/explore'); - - const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); - await expect(dataSourcePicker).toBeVisible(); - await dataSourcePicker.click(); - - const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); - await expect(datasourceOption).toBeVisible(); - await datasourceOption.scrollIntoViewIfNeeded(); - await datasourceOption.click(); - - // make sure the datasource was correctly selected and rendered - const breadcrumb = dashboardPage.getByGrafanaSelector( - selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) - ); - await expect(breadcrumb).toBeVisible(); - - const valueToType = 'test' + random(100); - - const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); - await expect(queryInput).not.toBeDisabled(); - await queryInput.fill(valueToType); - await expect(queryInput).toHaveValue(valueToType); - - // typing the query editor should reflect in the url - await expect(page).toHaveURL(new RegExp(valueToType)); - }); + // typing the query editor should reflect in the url + await expect(page).toHaveURL(new RegExp(valueToType)); }); }); diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 14148797750..f5f6a9116b6 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -165,10 +165,6 @@ export interface FeatureToggles { */ extraThemes?: boolean; /** - * Enables the plugins frontend sandbox - */ - pluginsFrontendSandbox?: boolean; - /** * Enables writing multiple items from a single query within Recorded Queries * @default true */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index be79b887b14..4834439df01 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -264,12 +264,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaFrontendPlatformSquad, }, - { - Name: "pluginsFrontendSandbox", - Description: "Enables the plugins frontend sandbox", - Stage: FeatureStagePrivatePreview, - Owner: grafanaPluginsPlatformSquad, - }, { Name: "recordedQueriesMulti", Description: "Enables writing multiple items from a single query within Recorded Queries", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 160802fc2db..6f75a9c077a 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -33,7 +33,6 @@ refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,true extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true -pluginsFrontendSandbox,privatePreview,@grafana/plugins-platform-backend,false,false,false recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true awsDatasourcesTempCredentials,GA,@grafana/aws-datasources,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index a82856cd292..2b568416f90 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -143,10 +143,6 @@ const ( // Enables extra themes FlagExtraThemes = "extraThemes" - // FlagPluginsFrontendSandbox - // Enables the plugins frontend sandbox - FlagPluginsFrontendSandbox = "pluginsFrontendSandbox" - // FlagRecordedQueriesMulti // Enables writing multiple items from a single query within Recorded Queries FlagRecordedQueriesMulti = "recordedQueriesMulti" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index ed9c80b988f..766e1e3a502 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3080,7 +3080,8 @@ "metadata": { "name": "pluginsFrontendSandbox", "resourceVersion": "1753448760331", - "creationTimestamp": "2023-06-05T08:51:36Z" + "creationTimestamp": "2023-06-05T08:51:36Z", + "deletionTimestamp": "2025-11-05T10:06:22Z" }, "spec": { "description": "Enables the plugins frontend sandbox", diff --git a/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.test.ts b/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.test.ts index a36480579d7..a6a4b0f8f57 100644 --- a/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.test.ts +++ b/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.test.ts @@ -15,7 +15,6 @@ import { jest.mock('@grafana/runtime', () => ({ config: { - featureToggles: { pluginsFrontendSandbox: true }, buildInfo: { env: 'production' }, enableFrontendSandboxForPlugins: [], }, @@ -53,7 +52,6 @@ describe('Sandbox eligibility checks', () => { setSandboxEnabledCheck(isPluginFrontendSandboxEnabled); config.enableFrontendSandboxForPlugins = []; - config.featureToggles.pluginsFrontendSandbox = true; process.env.NODE_ENV = 'development'; }); @@ -67,12 +65,6 @@ describe('Sandbox eligibility checks', () => { expect(isEligible).toBe(false); }); - test('shouldLoadPluginInFrontendSandbox returns false when feature toggle is off', async () => { - config.featureToggles.pluginsFrontendSandbox = false; - const result = await shouldLoadPluginInFrontendSandbox({ pluginId: 'test-plugin' }); - expect(result).toBe(false); - }); - test('setSandboxEnabledCheck sets custom check function', async () => { getPluginDetailsMock.mockResolvedValue(fakePluginDetails); const customCheck = jest.fn().mockResolvedValue(true); diff --git a/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.ts b/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.ts index 3aa489a35a9..e85a4d08ceb 100644 --- a/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.ts +++ b/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.ts @@ -35,11 +35,6 @@ export async function shouldLoadPluginInFrontendSandbox({ pluginId }: SandboxEli * It does not check if the plugin is actually enabled for the sandbox. */ export async function isPluginFrontendSandboxEligible({ pluginId }: SandboxEligibilityCheckParams): Promise { - // Only if the feature is not enabled no support for sandbox - if (!Boolean(config.featureToggles.pluginsFrontendSandbox)) { - return false; - } - // To fast-test and debug the sandbox in the browser (dev mode only). const sandboxDisableQueryParam = window.location.search.includes('nosandbox') && config.buildInfo.env === 'development';