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