diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx index 7d99dea4247..d78b4971bfe 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx @@ -3,9 +3,12 @@ import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; import { selectors } from '@grafana/e2e-selectors'; +import { config } from '@grafana/runtime'; import { sceneGraph, SceneRefreshPicker } from '@grafana/scenes'; +import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; import { SaveDashboardResponseDTO } from 'app/types'; +import { DashboardSceneState } from '../scene/DashboardScene'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; @@ -152,6 +155,70 @@ describe('SaveDashboardDrawer', () => { }); }); + describe('When a dashboard is managed by an external system', () => { + beforeEach(() => { + config.featureToggles.provisioning = true; + }); + + afterEach(() => { + config.featureToggles.provisioning = false; + }); + + it('It should show the changes tab if the resource can be edited', async () => { + const { dashboard, openAndRender } = setup({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Repo, + }, + }, + }, + }); + + // just changing the title here, in real case scenario changes are reflected through migrations + // eg. panel version - same for other manager tests below + dashboard.setState({ title: 'updated title' }); + openAndRender(); + + expect(screen.queryByRole('tab', { name: /Changes/ })).toBeInTheDocument(); + }); + + it('It should not show the changes tab if the resource cannot be edited; kubectl', async () => { + const { dashboard, openAndRender } = setup({ + meta: { k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Kubectl } } }, + }); + + dashboard.setState({ title: 'updated title' }); + openAndRender(); + + expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument(); + }); + + it('It should not show the changes tab if the resource cannot be edited; terraform', async () => { + const { dashboard, openAndRender } = setup({ + meta: { k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Terraform } } }, + }); + + dashboard.setState({ title: 'updated title' }); + openAndRender(); + + expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument(); + }); + + it('It should not show the changes tab if the resource cannot be edited; plugin', async () => { + const { dashboard, openAndRender } = setup({ + meta: { + k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Plugin } }, + }, + }); + + dashboard.setState({ title: 'updated title' }); + openAndRender(); + + expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument(); + }); + }); + describe('Save as copy', () => { it('Should show save as form', async () => { const { openAndRender } = setup(); @@ -199,7 +266,7 @@ function mockSaveDashboard(options: Partial = {}) { let cleanUp = () => {}; -function setup() { +function setup(overrides?: Partial) { const dashboard = transformSaveModelToScene({ dashboard: { title: 'hello', @@ -209,6 +276,7 @@ function setup() { version: 10, }, meta: {}, + ...overrides, }); // Clear any data layers diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx index 9bb2f70c3e7..dbc01acf19c 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx @@ -56,6 +56,7 @@ export class SaveDashboardDrawer extends SceneObjectBase model.setState({ showDiff: false })} /> - {changesCount > 0 && ( + {changesCount > 0 && !managedResourceCannotBeEdited && ( ; } - if (isProvisioned) { + if (isProvisioned || managedResourceCannotBeEdited) { return ; } diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 217816dd01d..ec90b73ab67 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -1,5 +1,5 @@ import { CoreApp, GrafanaConfig, LoadingState, getDefaultTimeRange, locationUtil, store } from '@grafana/data'; -import { locationService, RefreshEvent } from '@grafana/runtime'; +import { config, locationService, RefreshEvent } from '@grafana/runtime'; import { sceneGraph, SceneGridLayout, @@ -15,6 +15,7 @@ import { import { Dashboard, DashboardCursorSync, LibraryPanel } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; +import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { VariablesChanged } from 'app/features/variables/types'; @@ -797,6 +798,82 @@ describe('DashboardScene', () => { } }); }); + + describe('When checking dashboard managed by an external system', () => { + beforeEach(() => { + config.featureToggles.provisioning = true; + }); + + afterEach(() => { + config.featureToggles.provisioning = false; + }); + + it('should return true if the dashboard is managed', () => { + const scene = buildTestScene({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Repo, + }, + }, + }, + }); + expect(scene.isManaged()).toBe(true); + }); + + it('dashboard should be editable if managed by repo', () => { + const scene = buildTestScene({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Repo, + }, + }, + }, + }); + expect(scene.managedResourceCannotBeEdited()).toBe(false); + }); + + it('dashboard should not be editable if managed by systems that do not allow edits: kubectl', () => { + const scene = buildTestScene({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Kubectl, + }, + }, + }, + }); + expect(scene.managedResourceCannotBeEdited()).toBe(true); + }); + + it('dashboard should not be editable if managed by systems that do not allow edits: terraform', () => { + const scene = buildTestScene({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: ManagerKind.Terraform, + }, + }, + }, + }); + expect(scene.managedResourceCannotBeEdited()).toBe(true); + }); + + it('dashboard should not be editable if managed by systems that do not allow edits: plugin', () => { + const scene = buildTestScene({ + meta: { + k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Plugin } }, + }, + }); + expect(scene.managedResourceCannotBeEdited()).toBe(true); + }); + + it('dashboard should be editable if not managed', () => { + const scene = buildTestScene(); + expect(scene.managedResourceCannotBeEdited()).toBe(false); + }); + }); }); function buildTestScene(overrides?: Partial) { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 1206b787fdd..718e9b87b3e 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -33,7 +33,13 @@ import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; -import { AnnoKeyManagerKind, AnnoKeySourcePath, ManagerKind, ResourceForCreate } from '../../apiserver/types'; +import { + AnnoKeyManagerAllowsEdits, + AnnoKeyManagerKind, + AnnoKeySourcePath, + ManagerKind, + ResourceForCreate, +} from '../../apiserver/types'; import { DashboardEditPane } from '../edit-pane/DashboardEditPane'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; @@ -311,7 +317,7 @@ export class DashboardScene extends SceneObjectBase impleme return; } - if (!this.state.isDirty || skipConfirm) { + if (!this.state.isDirty || skipConfirm || this.managedResourceCannotBeEdited()) { this.exitEditModeConfirmed(restoreInitialState || this.state.isDirty); return; } @@ -804,6 +810,12 @@ export class DashboardScene extends SceneObjectBase impleme return Boolean(this.getManagerKind() === ManagerKind.Repo); } + managedResourceCannotBeEdited() { + return ( + this.isManaged() && !this.isManagedRepository() && !this.state.meta.k8s?.annotations?.[AnnoKeyManagerAllowsEdits] + ); + } + getPath() { return this.state.meta.k8s?.annotations?.[AnnoKeySourcePath]; }