From d70d9392944080527d976d90849d405f58717bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 20 Nov 2023 18:19:30 +0100 Subject: [PATCH] DashboardScene: Basics stubs for starting with editviews (dashboard settings) (#78209) --- .../scene/DashboardControls.tsx | 9 ++- .../dashboard-scene/scene/DashboardScene.tsx | 21 ++++--- .../scene/DashboardSceneRenderer.tsx | 14 ++--- .../scene/DashboardSceneUrlSync.ts | 28 +++++++-- .../scene/NavToolbarActions.tsx | 6 +- .../scene/keyboardShortcuts.ts | 6 ++ .../settings/AnnotationsEditView.tsx | 31 ++++++++++ .../settings/GeneralSettings.tsx | 33 ++++++++++ .../settings/VariablesEditView.tsx | 31 ++++++++++ .../dashboard-scene/settings/utils.ts | 60 +++++++++++++++++++ 10 files changed, 216 insertions(+), 23 deletions(-) create mode 100644 public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx create mode 100644 public/app/features/dashboard-scene/settings/GeneralSettings.tsx create mode 100644 public/app/features/dashboard-scene/settings/VariablesEditView.tsx create mode 100644 public/app/features/dashboard-scene/settings/utils.ts diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index e7c79b0ae2f..32e161338b1 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { SceneObjectState, SceneObject, SceneObjectBase, SceneComponentProps } from '@grafana/scenes'; -import { Box, Stack } from '@grafana/ui'; +import { Box, Stack, ToolbarButton } from '@grafana/ui'; + +import { getDashboardSceneFor } from '../utils/utils'; import { DashboardLinksControls } from './DashboardLinksControls'; @@ -15,7 +17,9 @@ export class DashboardControls extends SceneObjectBase { } function DashboardControlsRenderer({ model }: SceneComponentProps) { + const dashboard = getDashboardSceneFor(model); const { variableControls, linkControls, timeControls } = model.useState(); + const { isEditing } = dashboard.useState(); return ( + {isEditing && ( + + )} {timeControls.map((c) => ( ))} diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 0f95d8456af..f55e838da0d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -1,7 +1,7 @@ import * as H from 'history'; import { Unsubscribable } from 'rxjs'; -import { CoreApp, DataQueryRequest, NavIndex, NavModelItem, UrlQueryMap } from '@grafana/data'; +import { CoreApp, DataQueryRequest, NavIndex, NavModelItem } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { getUrlSyncManager, @@ -25,6 +25,7 @@ import { DashboardMeta } from 'app/types'; import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer'; import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer'; +import { DashboardEditView } from '../settings/utils'; import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; import { getDashboardUrl } from '../utils/urlBuilders'; import { findVizPanelByKey, forceRenderChildren, getClosestVizPanel, getPanelIdForVizPanel } from '../utils/utils'; @@ -55,6 +56,8 @@ export interface DashboardSceneState extends SceneObjectState { inspectPanelKey?: string; /** Panel to view in full screen */ viewPanelKey?: string; + /** Edit view */ + editview?: DashboardEditView; /** Scene object that handles the current drawer or modal */ overlay?: SceneObject; } @@ -78,7 +81,7 @@ export class DashboardScene extends SceneObjectBase { /** * Url state before editing started */ - private _initiallUrlState?: UrlQueryMap; + private _initialUrlState?: H.Location; /** * change tracking subscription */ @@ -129,7 +132,7 @@ export class DashboardScene extends SceneObjectBase { public onEnterEditMode = () => { // Save this state this._initialState = sceneUtils.cloneSceneObjectState(this.state); - this._initiallUrlState = locationService.getSearchObject(); + this._initialUrlState = locationService.getLocation(); // Switch to edit mode this.setState({ isEditing: true }); @@ -149,7 +152,7 @@ export class DashboardScene extends SceneObjectBase { // Stop url sync before updating url this.stopUrlSync(); // Now we can update url - locationService.partial(this._initiallUrlState!, true); + locationService.replace({ pathname: this._initialUrlState?.pathname, search: this._initialUrlState?.search }); // Update state and disable editing this.setState({ ...this._initialState, isEditing: false }); // and start url sync again @@ -167,14 +170,14 @@ export class DashboardScene extends SceneObjectBase { }; public getPageNav(location: H.Location, navIndex: NavIndex) { - const { meta } = this.state; + const { meta, viewPanelKey } = this.state; let pageNav: NavModelItem = { text: this.state.title, url: getDashboardUrl({ uid: this.state.uid, currentQueryParams: location.search, - updateQuery: { viewPanel: null, inspect: null }, + updateQuery: { viewPanel: null, inspect: null, editview: null }, useExperimentalURL: Boolean(config.featureToggles.dashboardSceneForViewers && meta.canEdit), }), }; @@ -205,7 +208,7 @@ export class DashboardScene extends SceneObjectBase { } } - if (this.state.viewPanelKey) { + if (viewPanelKey) { pageNav = { text: 'View panel', parentItem: pageNav, @@ -275,6 +278,10 @@ export class DashboardScene extends SceneObjectBase { } } + public onOpenSettings = () => { + locationService.partial({ editview: 'settings' }); + }; + /** * Called by the SceneQueryRunner to privide contextural parameters (tracking) props for the request */ diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 74943eb95fb..acc267b81f6 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneDebugger } from '@grafana/scenes'; import { CustomScrollbar, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; @@ -14,19 +13,20 @@ import { DashboardScene } from './DashboardScene'; import { NavToolbarActions } from './NavToolbarActions'; export function DashboardSceneRenderer({ model }: SceneComponentProps) { - const { controls, viewPanelKey: viewPanelId, overlay } = model.useState(); + const { controls, viewPanelKey, overlay, editview } = model.useState(); const styles = useStyles2(getStyles); const location = useLocation(); const navIndex = useSelector((state) => state.navIndex); const pageNav = model.getPageNav(location, navIndex); - const bodyToRender = model.getBodyToRender(viewPanelId); + const bodyToRender = model.getBodyToRender(viewPanelKey); + const navModel = getNavModel(navIndex, 'dashboards/browse'); - const navProps = config.featureToggles.dashboardSceneForViewers - ? { navModel: getNavModel(navIndex, 'dashboards/browse') } - : { navId: 'scenes' }; + if (editview) { + return ; + } return ( - +
diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 67e09082a4b..9954dc9322a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -6,6 +6,7 @@ import { SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '@grafana/scenes import appEvents from 'app/core/app_events'; import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer'; +import { createDashboardEditViewFor } from '../settings/utils'; import { findVizPanelByKey } from '../utils/utils'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; @@ -17,18 +18,35 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { constructor(private _scene: DashboardScene) {} getKeys(): string[] { - return ['inspect', 'viewPanel']; + return ['inspect', 'viewPanel', 'editview']; } getUrlState(): SceneObjectUrlValues { const state = this._scene.state; - return { inspect: state.inspectPanelKey, viewPanel: state.viewPanelKey }; + return { + inspect: state.inspectPanelKey, + viewPanel: state.viewPanelKey, + editview: state.editview?.getUrlKey(), + }; } updateFromUrl(values: SceneObjectUrlValues): void { - const { inspectPanelKey: inspectPanelId, viewPanelKey: viewPanelId } = this._scene.state; + const { inspectPanelKey, viewPanelKey, meta, isEditing } = this._scene.state; const update: Partial = {}; + if (typeof values.editview === 'string' && meta.canEdit) { + update.editview = createDashboardEditViewFor(values.editview); + + // If we are not in editing (for example after full page reload) + if (!isEditing) { + // Not sure what is best to do here. + // The reason for the timeout is for this change to happen after the url sync has completed + setTimeout(() => this._scene.onEnterEditMode()); + } + } else if (values.hasOwnProperty('editview')) { + update.editview = undefined; + } + // Handle inspect object state if (typeof values.inspect === 'string') { const panel = findVizPanelByKey(this._scene, values.inspect); @@ -40,7 +58,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { update.inspectPanelKey = values.inspect; update.overlay = new PanelInspectDrawer({ panelRef: panel.getRef() }); - } else if (inspectPanelId) { + } else if (inspectPanelKey) { update.inspectPanelKey = undefined; update.overlay = undefined; } @@ -61,7 +79,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { } update.viewPanelKey = values.viewPanel; - } else if (viewPanelId) { + } else if (viewPanelKey) { update.viewPanelKey = undefined; } diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 6f5b4285c67..8a85b652357 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -16,10 +16,10 @@ interface Props { } export const NavToolbarActions = React.memo(({ dashboard }) => { - const { actions = [], isEditing, viewPanelKey, isDirty, uid, meta } = dashboard.useState(); + const { actions = [], isEditing, viewPanelKey, isDirty, uid, meta, editview } = dashboard.useState(); const toolbarActions = (actions ?? []).map((action) => ); - if (uid) { + if (uid && !editview) { if (meta.canStar) { let desc = meta.isStarred ? t('dashboard.toolbar.unmark-favorite', 'Unmark as favorite') @@ -101,7 +101,7 @@ export const NavToolbarActions = React.memo(({ dashboard }) => { ); toolbarActions.push( - ); diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index c3c668f2074..0f306180db1 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -79,6 +79,12 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { onTrigger: () => sceneGraph.getTimeRange(scene).onRefresh(), }); + // Dashboard settings + keybindings.addBinding({ + key: 'd s', + onTrigger: scene.onOpenSettings, + }); + // toggle all panel legends (TODO) // delete panel (TODO when we work on editing) // toggle all exemplars (TODO) diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx new file mode 100644 index 00000000000..7a9244d52ce --- /dev/null +++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { PageLayoutType } from '@grafana/data'; +import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { Page } from 'app/core/components/Page/Page'; + +import { NavToolbarActions } from '../scene/NavToolbarActions'; +import { getDashboardSceneFor } from '../utils/utils'; + +import { GeneralSettingsEditView } from './GeneralSettings'; +import { DashboardEditView, useDashboardEditPageNav } from './utils'; + +export interface AnnotationsEditViewState extends SceneObjectState {} + +export class AnnotationsEditView extends SceneObjectBase implements DashboardEditView { + public getUrlKey(): string { + return 'annotations'; + } + + static Component = ({ model }: SceneComponentProps) => { + const dashboard = getDashboardSceneFor(model); + const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); + + return ( + + +
Annotations todo
+
+ ); + }; +} diff --git a/public/app/features/dashboard-scene/settings/GeneralSettings.tsx b/public/app/features/dashboard-scene/settings/GeneralSettings.tsx new file mode 100644 index 00000000000..f5f2943434e --- /dev/null +++ b/public/app/features/dashboard-scene/settings/GeneralSettings.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import { PageLayoutType } from '@grafana/data'; +import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { Page } from 'app/core/components/Page/Page'; + +import { NavToolbarActions } from '../scene/NavToolbarActions'; +import { getDashboardSceneFor } from '../utils/utils'; + +import { DashboardEditView, useDashboardEditPageNav } from './utils'; + +export interface GeneralSettingsEditViewState extends SceneObjectState {} + +export class GeneralSettingsEditView + extends SceneObjectBase + implements DashboardEditView +{ + public getUrlKey(): string { + return 'settings'; + } + + static Component = ({ model }: SceneComponentProps) => { + const dashboard = getDashboardSceneFor(model); + const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); + + return ( + + +
General todo
+
+ ); + }; +} diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx new file mode 100644 index 00000000000..e95482a7a60 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { PageLayoutType } from '@grafana/data'; +import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { Page } from 'app/core/components/Page/Page'; + +import { NavToolbarActions } from '../scene/NavToolbarActions'; +import { getDashboardSceneFor } from '../utils/utils'; + +import { GeneralSettingsEditView } from './GeneralSettings'; +import { DashboardEditView, useDashboardEditPageNav } from './utils'; + +export interface VariablesEditViewState extends SceneObjectState {} + +export class VariablesEditView extends SceneObjectBase implements DashboardEditView { + public getUrlKey(): string { + return 'variables'; + } + + static Component = ({ model }: SceneComponentProps) => { + const dashboard = getDashboardSceneFor(model); + const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); + + return ( + + +
variables todo
+
+ ); + }; +} diff --git a/public/app/features/dashboard-scene/settings/utils.ts b/public/app/features/dashboard-scene/settings/utils.ts new file mode 100644 index 00000000000..5853ccde071 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/utils.ts @@ -0,0 +1,60 @@ +import { useLocation } from 'react-router-dom'; + +import { locationUtil, NavModelItem } from '@grafana/data'; +import { SceneObject } from '@grafana/scenes'; +import { t } from 'app/core/internationalization'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { useSelector } from 'app/types'; + +import { DashboardScene } from '../scene/DashboardScene'; + +import { AnnotationsEditView } from './AnnotationsEditView'; +import { GeneralSettingsEditView } from './GeneralSettings'; +import { VariablesEditView } from './VariablesEditView'; + +export interface DashboardEditView extends SceneObject { + getUrlKey(): string; +} + +export function useDashboardEditPageNav(dashboard: DashboardScene, currentEditView: string) { + const location = useLocation(); + const navIndex = useSelector((state) => state.navIndex); + const navModel = getNavModel(navIndex, 'dashboards/browse'); + const dashboardPageNav = dashboard.getPageNav(location, navIndex); + + const pageNav: NavModelItem = { + text: 'Settings', + children: [ + { + text: t('dashboard-settings.general.title', 'General'), + url: locationUtil.getUrlForPartial(location, { editview: 'settings', editIndex: null }), + active: currentEditView === 'settings', + }, + { + text: t('dashboard-settings.annotations.title', 'Annotations'), + url: locationUtil.getUrlForPartial(location, { editview: 'annotations', editIndex: null }), + active: currentEditView === 'annotations', + }, + { + text: t('dashboard-settings.variables.title', 'Variables'), + url: locationUtil.getUrlForPartial(location, { editview: 'variables', editIndex: null }), + active: currentEditView === 'variables', + }, + ], + parentItem: dashboardPageNav, + }; + + return { navModel, pageNav }; +} + +export function createDashboardEditViewFor(editview: string): DashboardEditView { + switch (editview) { + case 'annotations': + return new AnnotationsEditView({}); + case 'variables': + return new VariablesEditView({}); + case 'settings': + default: + return new GeneralSettingsEditView({}); + } +}