mirror of https://github.com/grafana/grafana.git
				
				
				
			Dashboards: Disable saving in the UI for provisioned k8s dashboards (#105429)
* disable editing in UI for k8s dashboards * lint * use AnnoKeyManagerAllowsEdits; clean up * fix * add tests; update the logic * clean up
This commit is contained in:
		
							parent
							
								
									0166b6bcc6
								
							
						
					
					
						commit
						c6ada816c2
					
				| 
						 | 
					@ -3,9 +3,12 @@ import userEvent from '@testing-library/user-event';
 | 
				
			||||||
import { TestProvider } from 'test/helpers/TestProvider';
 | 
					import { TestProvider } from 'test/helpers/TestProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { selectors } from '@grafana/e2e-selectors';
 | 
					import { selectors } from '@grafana/e2e-selectors';
 | 
				
			||||||
 | 
					import { config } from '@grafana/runtime';
 | 
				
			||||||
import { sceneGraph, SceneRefreshPicker } from '@grafana/scenes';
 | 
					import { sceneGraph, SceneRefreshPicker } from '@grafana/scenes';
 | 
				
			||||||
 | 
					import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types';
 | 
				
			||||||
import { SaveDashboardResponseDTO } from 'app/types';
 | 
					import { SaveDashboardResponseDTO } from 'app/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DashboardSceneState } from '../scene/DashboardScene';
 | 
				
			||||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
 | 
					import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
 | 
				
			||||||
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
 | 
					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', () => {
 | 
					  describe('Save as copy', () => {
 | 
				
			||||||
    it('Should show save as form', async () => {
 | 
					    it('Should show save as form', async () => {
 | 
				
			||||||
      const { openAndRender } = setup();
 | 
					      const { openAndRender } = setup();
 | 
				
			||||||
| 
						 | 
					@ -199,7 +266,7 @@ function mockSaveDashboard(options: Partial<MockBackendApiOptions> = {}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let cleanUp = () => {};
 | 
					let cleanUp = () => {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setup() {
 | 
					function setup(overrides?: Partial<DashboardSceneState>) {
 | 
				
			||||||
  const dashboard = transformSaveModelToScene({
 | 
					  const dashboard = transformSaveModelToScene({
 | 
				
			||||||
    dashboard: {
 | 
					    dashboard: {
 | 
				
			||||||
      title: 'hello',
 | 
					      title: 'hello',
 | 
				
			||||||
| 
						 | 
					@ -209,6 +276,7 @@ function setup() {
 | 
				
			||||||
      version: 10,
 | 
					      version: 10,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    meta: {},
 | 
					    meta: {},
 | 
				
			||||||
 | 
					    ...overrides,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Clear any data layers
 | 
					  // Clear any data layers
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,6 +56,7 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
 | 
				
			||||||
    const dashboard = model.state.dashboardRef.resolve();
 | 
					    const dashboard = model.state.dashboardRef.resolve();
 | 
				
			||||||
    const { meta } = dashboard.useState();
 | 
					    const { meta } = dashboard.useState();
 | 
				
			||||||
    const { provisioned: isProvisioned, folderTitle } = meta;
 | 
					    const { provisioned: isProvisioned, folderTitle } = meta;
 | 
				
			||||||
 | 
					    const managedResourceCannotBeEdited = dashboard.managedResourceCannotBeEdited();
 | 
				
			||||||
    const isProvisionedNG = useIsProvisionedNG(dashboard);
 | 
					    const isProvisionedNG = useIsProvisionedNG(dashboard);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const tabs = (
 | 
					    const tabs = (
 | 
				
			||||||
| 
						 | 
					@ -65,7 +66,7 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
 | 
				
			||||||
          active={!showDiff}
 | 
					          active={!showDiff}
 | 
				
			||||||
          onChangeTab={() => model.setState({ showDiff: false })}
 | 
					          onChangeTab={() => model.setState({ showDiff: false })}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        {changesCount > 0 && (
 | 
					        {changesCount > 0 && !managedResourceCannotBeEdited && (
 | 
				
			||||||
          <Tab
 | 
					          <Tab
 | 
				
			||||||
            label={t('dashboard-scene.save-dashboard-drawer.tabs.label-changes', 'Changes')}
 | 
					            label={t('dashboard-scene.save-dashboard-drawer.tabs.label-changes', 'Changes')}
 | 
				
			||||||
            active={showDiff}
 | 
					            active={showDiff}
 | 
				
			||||||
| 
						 | 
					@ -106,7 +107,7 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
 | 
				
			||||||
        return <SaveDashboardAsForm dashboard={dashboard} changeInfo={changeInfo} />;
 | 
					        return <SaveDashboardAsForm dashboard={dashboard} changeInfo={changeInfo} />;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (isProvisioned) {
 | 
					      if (isProvisioned || managedResourceCannotBeEdited) {
 | 
				
			||||||
        return <SaveProvisionedDashboardForm dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
 | 
					        return <SaveProvisionedDashboardForm dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { CoreApp, GrafanaConfig, LoadingState, getDefaultTimeRange, locationUtil, store } from '@grafana/data';
 | 
					import { CoreApp, GrafanaConfig, LoadingState, getDefaultTimeRange, locationUtil, store } from '@grafana/data';
 | 
				
			||||||
import { locationService, RefreshEvent } from '@grafana/runtime';
 | 
					import { config, locationService, RefreshEvent } from '@grafana/runtime';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  sceneGraph,
 | 
					  sceneGraph,
 | 
				
			||||||
  SceneGridLayout,
 | 
					  SceneGridLayout,
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,7 @@ import {
 | 
				
			||||||
import { Dashboard, DashboardCursorSync, LibraryPanel } from '@grafana/schema';
 | 
					import { Dashboard, DashboardCursorSync, LibraryPanel } from '@grafana/schema';
 | 
				
			||||||
import appEvents from 'app/core/app_events';
 | 
					import appEvents from 'app/core/app_events';
 | 
				
			||||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
 | 
					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 { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
 | 
				
			||||||
import { VariablesChanged } from 'app/features/variables/types';
 | 
					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<DashboardSceneState>) {
 | 
					function buildTestScene(overrides?: Partial<DashboardSceneState>) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,13 @@ import { VariablesChanged } from 'app/features/variables/types';
 | 
				
			||||||
import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types';
 | 
					import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types';
 | 
				
			||||||
import { ShowConfirmModalEvent } from 'app/types/events';
 | 
					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 { DashboardEditPane } from '../edit-pane/DashboardEditPane';
 | 
				
			||||||
import { PanelEditor } from '../panel-edit/PanelEditor';
 | 
					import { PanelEditor } from '../panel-edit/PanelEditor';
 | 
				
			||||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
 | 
					import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
 | 
				
			||||||
| 
						 | 
					@ -311,7 +317,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!this.state.isDirty || skipConfirm) {
 | 
					    if (!this.state.isDirty || skipConfirm || this.managedResourceCannotBeEdited()) {
 | 
				
			||||||
      this.exitEditModeConfirmed(restoreInitialState || this.state.isDirty);
 | 
					      this.exitEditModeConfirmed(restoreInitialState || this.state.isDirty);
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -804,6 +810,12 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
 | 
				
			||||||
    return Boolean(this.getManagerKind() === ManagerKind.Repo);
 | 
					    return Boolean(this.getManagerKind() === ManagerKind.Repo);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  managedResourceCannotBeEdited() {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      this.isManaged() && !this.isManagedRepository() && !this.state.meta.k8s?.annotations?.[AnnoKeyManagerAllowsEdits]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getPath() {
 | 
					  getPath() {
 | 
				
			||||||
    return this.state.meta.k8s?.annotations?.[AnnoKeySourcePath];
 | 
					    return this.state.meta.k8s?.annotations?.[AnnoKeySourcePath];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue