DashboardScene: Basics stubs for starting with editviews (dashboard settings) (#78209)

This commit is contained in:
Torkel Ödegaard 2023-11-20 18:19:30 +01:00 committed by GitHub
parent 97c8b2d192
commit d70d939294
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 216 additions and 23 deletions

View File

@ -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<DashboardControlsState> {
}
function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardControls>) {
const dashboard = getDashboardSceneFor(model);
const { variableControls, linkControls, timeControls } = model.useState();
const { isEditing } = dashboard.useState();
return (
<Stack
@ -33,6 +37,9 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
<linkControls.Component model={linkControls} />
</Stack>
<Stack justifyContent={'flex-end'}>
{isEditing && (
<ToolbarButton variant="canvas" icon="cog" tooltip="Dashboard settings" onClick={dashboard.onOpenSettings} />
)}
{timeControls.map((c) => (
<c.Component model={c} key={c.state.key} />
))}

View File

@ -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<DashboardSceneState> {
/**
* Url state before editing started
*/
private _initiallUrlState?: UrlQueryMap;
private _initialUrlState?: H.Location;
/**
* change tracking subscription
*/
@ -129,7 +132,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
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<DashboardSceneState> {
// 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<DashboardSceneState> {
};
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<DashboardSceneState> {
}
}
if (this.state.viewPanelKey) {
if (viewPanelKey) {
pageNav = {
text: 'View panel',
parentItem: pageNav,
@ -275,6 +278,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
}
}
public onOpenSettings = () => {
locationService.partial({ editview: 'settings' });
};
/**
* Called by the SceneQueryRunner to privide contextural parameters (tracking) props for the request
*/

View File

@ -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<DashboardScene>) {
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 <editview.Component model={editview} />;
}
return (
<Page {...navProps} pageNav={pageNav} layout={PageLayoutType.Custom}>
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
<CustomScrollbar autoHeightMin={'100%'}>
<div className={styles.canvasContent}>
<NavToolbarActions dashboard={model} />

View File

@ -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<DashboardSceneState> = {};
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;
}

View File

@ -16,10 +16,10 @@ interface Props {
}
export const NavToolbarActions = React.memo<Props>(({ 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) => <action.Component key={action.state.key} model={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<Props>(({ dashboard }) => {
</Button>
);
toolbarActions.push(
<Button onClick={dashboard.onDiscard} tooltip="Save changes" fill="text" key="discard" variant="destructive">
<Button onClick={dashboard.onDiscard} tooltip="Discard changes" fill="text" key="discard" variant="destructive">
Discard
</Button>
);

View File

@ -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)

View File

@ -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<AnnotationsEditViewState> implements DashboardEditView {
public getUrlKey(): string {
return 'annotations';
}
static Component = ({ model }: SceneComponentProps<GeneralSettingsEditView>) => {
const dashboard = getDashboardSceneFor(model);
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={dashboard} />
<div>Annotations todo</div>
</Page>
);
};
}

View File

@ -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<GeneralSettingsEditViewState>
implements DashboardEditView
{
public getUrlKey(): string {
return 'settings';
}
static Component = ({ model }: SceneComponentProps<GeneralSettingsEditView>) => {
const dashboard = getDashboardSceneFor(model);
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={dashboard} />
<div>General todo</div>
</Page>
);
};
}

View File

@ -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<VariablesEditViewState> implements DashboardEditView {
public getUrlKey(): string {
return 'variables';
}
static Component = ({ model }: SceneComponentProps<GeneralSettingsEditView>) => {
const dashboard = getDashboardSceneFor(model);
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={dashboard} />
<div>variables todo</div>
</Page>
);
};
}

View File

@ -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({});
}
}