mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SceneDashboard: Move time controls from nav toolbar into controls and make controls them sticky, and edit mode (#71082)
* Scene with sticky controls * Progress on an edit mode
This commit is contained in:
parent
1b80df0168
commit
d87c2c4049
@ -196,6 +196,17 @@ export const DashNav = React.memo<Props>((props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.featureToggles.scenes) {
|
||||||
|
buttons.push(
|
||||||
|
<DashNavButton
|
||||||
|
key="button-scenes"
|
||||||
|
tooltip={'View as Scene'}
|
||||||
|
icon="apps"
|
||||||
|
onClick={() => locationService.push(`/scenes/dashboard/${dashboard.uid}`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
addCustomContent(customLeftActions, buttons);
|
addCustomContent(customLeftActions, buttons);
|
||||||
return buttons;
|
return buttons;
|
||||||
};
|
};
|
||||||
@ -310,16 +321,6 @@ export const DashNav = React.memo<Props>((props) => {
|
|||||||
|
|
||||||
buttons.push(renderTimeControls());
|
buttons.push(renderTimeControls());
|
||||||
|
|
||||||
if (config.featureToggles.scenes) {
|
|
||||||
buttons.push(
|
|
||||||
<ToolbarButton
|
|
||||||
key="button-scenes"
|
|
||||||
tooltip={'View as Scene'}
|
|
||||||
icon="apps"
|
|
||||||
onClick={() => locationService.push(`/scenes/dashboard/${dashboard.uid}`)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return buttons;
|
return buttons;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,72 +1,55 @@
|
|||||||
import { css } from '@emotion/css';
|
import {
|
||||||
import React from 'react';
|
getUrlSyncManager,
|
||||||
|
SceneGridItem,
|
||||||
|
SceneObject,
|
||||||
|
SceneObjectBase,
|
||||||
|
SceneObjectState,
|
||||||
|
SceneObjectStateChangedEvent,
|
||||||
|
} from '@grafana/scenes';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
import { DashboardSceneRenderer } from './DashboardSceneRenderer';
|
||||||
import { locationService } from '@grafana/runtime';
|
|
||||||
import { SceneObjectBase, SceneComponentProps, SceneObject, SceneObjectState } from '@grafana/scenes';
|
|
||||||
import { ToolbarButton, useStyles2 } from '@grafana/ui';
|
|
||||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
|
||||||
|
|
||||||
interface DashboardSceneState extends SceneObjectState {
|
export interface DashboardSceneState extends SceneObjectState {
|
||||||
title: string;
|
title: string;
|
||||||
uid?: string;
|
uid?: string;
|
||||||
body: SceneObject;
|
body: SceneObject;
|
||||||
actions?: SceneObject[];
|
actions?: SceneObject[];
|
||||||
controls?: SceneObject[];
|
controls?: SceneObject[];
|
||||||
|
isEditing?: boolean;
|
||||||
|
isDirty?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||||
static Component = DashboardSceneRenderer;
|
static Component = DashboardSceneRenderer;
|
||||||
}
|
|
||||||
|
|
||||||
function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
constructor(state: DashboardSceneState) {
|
||||||
const { title, body, actions = [], uid, controls } = model.useState();
|
super(state);
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
|
this.addActivationHandler(() => {
|
||||||
|
return () => getUrlSyncManager().cleanUp(this);
|
||||||
|
});
|
||||||
|
|
||||||
if (uid?.length) {
|
this.subscribeToEvent(SceneObjectStateChangedEvent, this.onChildStateChanged);
|
||||||
toolbarActions.push(
|
|
||||||
<ToolbarButton
|
|
||||||
icon="apps"
|
|
||||||
onClick={() => locationService.push(`/d/${uid}`)}
|
|
||||||
tooltip="View as Dashboard"
|
|
||||||
key="scene-to-dashboard-switch"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
onChildStateChanged = (event: SceneObjectStateChangedEvent) => {
|
||||||
<Page navId="scenes" pageNav={{ text: title }} layout={PageLayoutType.Canvas}>
|
// Temporary hacky way to detect changes
|
||||||
<AppChromeUpdate actions={toolbarActions} />
|
if (event.payload.changedObject instanceof SceneGridItem) {
|
||||||
{controls && (
|
this.setState({ isDirty: true });
|
||||||
<div className={styles.controls}>
|
}
|
||||||
{controls.map((control) => (
|
};
|
||||||
<control.Component key={control.state.key} model={control} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.body}>
|
|
||||||
<body.Component model={body} />
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2) {
|
initUrlSync() {
|
||||||
return {
|
getUrlSyncManager().initSync(this);
|
||||||
body: css({
|
}
|
||||||
flexGrow: 1,
|
|
||||||
display: 'flex',
|
onEnterEditMode = () => {
|
||||||
gap: '8px',
|
this.setState({ isEditing: true });
|
||||||
}),
|
};
|
||||||
controls: css({
|
|
||||||
display: 'flex',
|
onDiscard = () => {
|
||||||
paddingBottom: theme.spacing(2),
|
// TODO open confirm modal if dirty
|
||||||
flexWrap: 'wrap',
|
// TODO actually discard changes
|
||||||
alignItems: 'center',
|
this.setState({ isEditing: false });
|
||||||
gap: theme.spacing(1),
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
114
public/app/features/scenes/dashboard/DashboardSceneRenderer.tsx
Normal file
114
public/app/features/scenes/dashboard/DashboardSceneRenderer.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
import { SceneComponentProps } from '@grafana/scenes';
|
||||||
|
import { Button, CustomScrollbar, useStyles2 } from '@grafana/ui';
|
||||||
|
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||||
|
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
|
||||||
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
|
||||||
|
|
||||||
|
import { DashboardScene } from './DashboardScene';
|
||||||
|
|
||||||
|
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
||||||
|
const { title, body, actions = [], controls, isEditing, isDirty, uid } = model.useState();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
|
||||||
|
|
||||||
|
if (uid) {
|
||||||
|
toolbarActions.push(
|
||||||
|
<DashNavButton
|
||||||
|
key="button-scenes"
|
||||||
|
tooltip={'View as dashboard'}
|
||||||
|
icon="apps"
|
||||||
|
onClick={() => locationService.push(`/d/${uid}`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbarActions.push(<NavToolbarSeparator leftActionsSeparator />);
|
||||||
|
|
||||||
|
if (!isEditing) {
|
||||||
|
// TODO check permissions
|
||||||
|
toolbarActions.push(
|
||||||
|
<Button
|
||||||
|
onClick={model.onEnterEditMode}
|
||||||
|
tooltip="Enter edit mode"
|
||||||
|
key="edit"
|
||||||
|
variant="primary"
|
||||||
|
icon="pen"
|
||||||
|
fill="text"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// TODO check permissions
|
||||||
|
toolbarActions.push(
|
||||||
|
<Button onClick={model.onEnterEditMode} tooltip="Save as copy" fill="text" key="save-as">
|
||||||
|
Save as
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
toolbarActions.push(
|
||||||
|
<Button onClick={model.onDiscard} tooltip="Save changes" fill="text" key="discard" variant="destructive">
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
toolbarActions.push(
|
||||||
|
<Button onClick={model.onEnterEditMode} tooltip="Save changes" key="save" disabled={!isDirty}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page navId="scenes" pageNav={{ text: title }} layout={PageLayoutType.Custom}>
|
||||||
|
<CustomScrollbar autoHeightMin={'100%'}>
|
||||||
|
<div className={styles.canvasContent}>
|
||||||
|
<AppChromeUpdate actions={toolbarActions} />
|
||||||
|
{controls && (
|
||||||
|
<div className={styles.controls}>
|
||||||
|
{controls.map((control) => (
|
||||||
|
<control.Component key={control.state.key} model={control} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.body}>
|
||||||
|
<body.Component model={body} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CustomScrollbar>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
|
return {
|
||||||
|
canvasContent: css({
|
||||||
|
label: 'canvas-content',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: theme.spacing(0, 2),
|
||||||
|
flexBasis: '100%',
|
||||||
|
flexGrow: 1,
|
||||||
|
}),
|
||||||
|
body: css({
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
}),
|
||||||
|
controls: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
background: theme.colors.background.canvas,
|
||||||
|
zIndex: 1,
|
||||||
|
padding: theme.spacing(2, 0),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
@ -24,6 +24,8 @@ import {
|
|||||||
SceneGridItem,
|
SceneGridItem,
|
||||||
SceneDataProvider,
|
SceneDataProvider,
|
||||||
getUrlSyncManager,
|
getUrlSyncManager,
|
||||||
|
SceneObject,
|
||||||
|
SceneControlsSpacer,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||||
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||||
@ -177,6 +179,16 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const controls: SceneObject[] = [
|
||||||
|
new VariableValueSelectors({}),
|
||||||
|
new SceneControlsSpacer(),
|
||||||
|
new SceneTimePicker({}),
|
||||||
|
new SceneRefreshPicker({
|
||||||
|
refresh: oldModel.refresh,
|
||||||
|
intervals: oldModel.timepicker.refresh_intervals,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
return new DashboardScene({
|
return new DashboardScene({
|
||||||
title: oldModel.title,
|
title: oldModel.title,
|
||||||
uid: oldModel.uid,
|
uid: oldModel.uid,
|
||||||
@ -184,17 +196,8 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
|||||||
children: createSceneObjectsForPanels(oldModel.panels),
|
children: createSceneObjectsForPanels(oldModel.panels),
|
||||||
}),
|
}),
|
||||||
$timeRange: new SceneTimeRange(oldModel.time),
|
$timeRange: new SceneTimeRange(oldModel.time),
|
||||||
actions: [
|
|
||||||
new SceneTimePicker({}),
|
|
||||||
new SceneRefreshPicker({
|
|
||||||
refresh: oldModel.refresh,
|
|
||||||
intervals: oldModel.timepicker.refresh_intervals,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
$variables: variables,
|
$variables: variables,
|
||||||
...(variables && {
|
controls: controls,
|
||||||
controls: [new VariableValueSelectors({})],
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user