mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Action toolbar progress (#81664)
* DashboardScene: Action toolbar progress * Add discard confirmation modal * minor fix * Update * tweaked * Updating * Progress * Update * Update * Added some unit tests * fix test * Change name to Exit edit * Tweaks * fix test * Minor margin fix * Move share to left of edit
This commit is contained in:
parent
9b18a4d45e
commit
61c7fcc270
@ -117,7 +117,7 @@ const getStyles = (theme: GrafanaTheme2, overflowButtonOrder: number, alignment:
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.colors.background.primary,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
boxShadow: theme.shadows.z3,
|
||||
boxShadow: theme.shadows.z2,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: theme.spacing(1),
|
||||
@ -128,7 +128,7 @@ const getStyles = (theme: GrafanaTheme2, overflowButtonOrder: number, alignment:
|
||||
right: 0,
|
||||
top: '100%',
|
||||
width: 'max-content',
|
||||
zIndex: theme.zIndex.sidemenu,
|
||||
zIndex: theme.zIndex.dropdown,
|
||||
}),
|
||||
container: css({
|
||||
alignItems: 'center',
|
||||
|
@ -62,7 +62,6 @@ export function NavToolbar({
|
||||
<Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
|
||||
<div className={styles.actions}>
|
||||
{actions}
|
||||
{actions && <NavToolbarSeparator />}
|
||||
{searchBarHidden && (
|
||||
<ToolbarButton
|
||||
onClick={onToggleKioskMode}
|
||||
@ -71,6 +70,7 @@ export function NavToolbar({
|
||||
icon="monitor"
|
||||
/>
|
||||
)}
|
||||
{actions && <NavToolbarSeparator />}
|
||||
<ToolbarButton
|
||||
onClick={onToggleSearchBar}
|
||||
narrow
|
||||
|
@ -12,7 +12,7 @@ export function initAlerting() {
|
||||
addCustomRightAction({
|
||||
show: () => config.unifiedAlertingEnabled,
|
||||
component: ({ dashboard }) => (
|
||||
<React.Suspense fallback={null}>
|
||||
<React.Suspense fallback={null} key="alert-rules-button">
|
||||
{dashboard && <AlertRulesToolbarButton dashboardUid={dashboard.uid} />}
|
||||
</React.Suspense>
|
||||
),
|
||||
|
@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SceneObjectState, SceneObject, SceneObjectBase, SceneComponentProps } from '@grafana/scenes';
|
||||
import { Box, Stack, ToolbarButton } from '@grafana/ui';
|
||||
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
import { Box, Stack } from '@grafana/ui';
|
||||
|
||||
import { DashboardLinksControls } from './DashboardLinksControls';
|
||||
|
||||
@ -18,9 +16,7 @@ export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
|
||||
}
|
||||
|
||||
function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardControls>) {
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const { variableControls, linkControls, timeControls, hideTimeControls } = model.useState();
|
||||
const { isEditing } = dashboard.useState();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
@ -38,9 +34,6 @@ 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} />
|
||||
)}
|
||||
{!hideTimeControls && timeControls.map((c) => <c.Component model={c} key={c.state.key} />)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
@ -57,7 +57,7 @@ describe('DashboardScene', () => {
|
||||
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
|
||||
scene.onDiscard();
|
||||
scene.exitEditMode({ skipConfirm: true });
|
||||
const gridItem2 = sceneGraph.findObject(scene, (p) => p.state.key === 'griditem-1') as SceneGridItem;
|
||||
expect(gridItem2.state.x).toBe(0);
|
||||
});
|
||||
@ -77,7 +77,7 @@ describe('DashboardScene', () => {
|
||||
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
|
||||
scene.onDiscard();
|
||||
scene.exitEditMode({ skipConfirm: true });
|
||||
expect(scene.state[prop]).toEqual(prevState);
|
||||
}
|
||||
);
|
||||
@ -89,7 +89,7 @@ describe('DashboardScene', () => {
|
||||
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
|
||||
scene.onDiscard();
|
||||
scene.exitEditMode({ skipConfirm: true });
|
||||
expect(dashboardSceneGraph.getRefreshPicker(scene)!.state.intervals).toEqual(prevState);
|
||||
});
|
||||
|
||||
@ -100,7 +100,7 @@ describe('DashboardScene', () => {
|
||||
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
|
||||
scene.onDiscard();
|
||||
scene.exitEditMode({ skipConfirm: true });
|
||||
expect(dashboardSceneGraph.getDashboardControls(scene)!.state.hideTimeControls).toEqual(prevState);
|
||||
});
|
||||
|
||||
@ -111,7 +111,7 @@ describe('DashboardScene', () => {
|
||||
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
|
||||
scene.onDiscard();
|
||||
scene.exitEditMode({ skipConfirm: true });
|
||||
expect(sceneGraph.getTimeRange(scene)!.state.timeZone).toBe(prevState);
|
||||
});
|
||||
});
|
||||
|
@ -26,6 +26,7 @@ import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { VariablesChanged } from 'app/features/variables/types';
|
||||
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types';
|
||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import { PanelEditor } from '../panel-edit/PanelEditor';
|
||||
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
|
||||
@ -213,12 +214,29 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
}
|
||||
}
|
||||
|
||||
public onDiscard = () => {
|
||||
public exitEditMode({ skipConfirm }: { skipConfirm: boolean }) {
|
||||
if (!this.canDiscard()) {
|
||||
console.error('Trying to discard back to a state that does not exist, initialState undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.isDirty || skipConfirm) {
|
||||
this.exitEditModeConfirmed();
|
||||
return;
|
||||
}
|
||||
|
||||
appEvents.publish(
|
||||
new ShowConfirmModalEvent({
|
||||
title: 'Discard changes to dashboard?',
|
||||
text: `You have unsaved changes to this dashboard. Are you sure you want to discard them?`,
|
||||
icon: 'trash-alt',
|
||||
yesText: 'Discard',
|
||||
onConfirm: this.exitEditModeConfirmed.bind(this),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private exitEditModeConfirmed() {
|
||||
// No need to listen to changes anymore
|
||||
this.stopTrackingChanges();
|
||||
// Stop url sync before updating url
|
||||
@ -244,7 +262,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
this.startUrlSync();
|
||||
// Disable grid dragging
|
||||
this.propagateEditModeChange();
|
||||
};
|
||||
}
|
||||
|
||||
public canDiscard() {
|
||||
return this._initialState !== undefined;
|
||||
@ -266,7 +284,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
newState.version = versionRsp.version;
|
||||
|
||||
this._initialState = newState;
|
||||
this.onDiscard();
|
||||
this.exitEditMode({ skipConfirm: false });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||
const pageNav = model.getPageNav(location, navIndex);
|
||||
const bodyToRender = model.getBodyToRender();
|
||||
const navModel = getNavModel(navIndex, 'dashboards/browse');
|
||||
const showDebugger = location.search.includes('scene-debugger');
|
||||
|
||||
if (editview) {
|
||||
return (
|
||||
@ -43,7 +44,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||
{controls.map((control) => (
|
||||
<control.Component key={control.state.key} model={control} />
|
||||
))}
|
||||
<SceneDebugger scene={model} key={'scene-debugger'} />
|
||||
{showDebugger && <SceneDebugger scene={model} key={'scene-debugger'} />}
|
||||
</div>
|
||||
)}
|
||||
<div className={cx(styles.body)}>
|
||||
@ -83,7 +84,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
background: theme.colors.background.canvas,
|
||||
zIndex: theme.zIndex.navbarFixed,
|
||||
zIndex: theme.zIndex.activePanel,
|
||||
padding: theme.spacing(2, 0),
|
||||
}),
|
||||
};
|
||||
|
@ -0,0 +1,83 @@
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
||||
|
||||
import { ToolbarActions } from './NavToolbarActions';
|
||||
|
||||
describe('NavToolbarActions', () => {
|
||||
describe('Give an already saved dashboard', () => {
|
||||
it('Should show correct buttons when not in editing', async () => {
|
||||
setup();
|
||||
|
||||
expect(screen.queryByText('Save dashboard')).not.toBeInTheDocument();
|
||||
expect(await screen.findByText('Edit')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Share')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show correct buttons when editing', async () => {
|
||||
setup();
|
||||
|
||||
await userEvent.click(await screen.findByText('Edit'));
|
||||
|
||||
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Exit edit')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Share')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show correct buttons when in settings menu', async () => {
|
||||
setup();
|
||||
|
||||
await userEvent.click(await screen.findByText('Edit'));
|
||||
await userEvent.click(await screen.findByText('Settings'));
|
||||
|
||||
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Back to dashboard')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let cleanUp = () => {};
|
||||
|
||||
function setup() {
|
||||
const dashboard = transformSaveModelToScene({
|
||||
dashboard: {
|
||||
title: 'hello',
|
||||
uid: 'my-uid',
|
||||
schemaVersion: 30,
|
||||
panels: [],
|
||||
version: 10,
|
||||
},
|
||||
meta: {
|
||||
canSave: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Clear any data layers
|
||||
dashboard.setState({ $data: undefined });
|
||||
|
||||
const initialSaveModel = transformSceneToSaveModel(dashboard);
|
||||
dashboard.setInitialSaveModel(initialSaveModel);
|
||||
|
||||
dashboard.startUrlSync();
|
||||
|
||||
cleanUp();
|
||||
cleanUp = dashboard.activate();
|
||||
|
||||
const context = getGrafanaContextMock();
|
||||
|
||||
render(
|
||||
<TestProvider grafanaContext={context}>
|
||||
<ToolbarActions dashboard={dashboard} />
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
const actions = context.chrome.state.getValue().actions;
|
||||
|
||||
return { dashboard, actions };
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { Button, ButtonGroup, Dropdown, Icon, Menu, ToolbarButton, ToolbarButtonRow, useStyles2 } from '@grafana/ui';
|
||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
@ -19,164 +21,300 @@ interface Props {
|
||||
}
|
||||
|
||||
export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||
const { actions = [], isEditing, viewPanelScene, isDirty, uid, meta, editview } = dashboard.useState();
|
||||
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
|
||||
const rightToolbarActions: JSX.Element[] = [];
|
||||
const _legacyDashboardModel = getDashboardSrv().getCurrent();
|
||||
const actions = (
|
||||
<ToolbarButtonRow alignment="right">
|
||||
<ToolbarActions dashboard={dashboard} />
|
||||
</ToolbarButtonRow>
|
||||
);
|
||||
|
||||
if (uid && !editview) {
|
||||
if (meta.canStar) {
|
||||
return <AppChromeUpdate actions={actions} />;
|
||||
});
|
||||
|
||||
NavToolbarActions.displayName = 'NavToolbarActions';
|
||||
|
||||
/**
|
||||
* This part is split into a separate componet to help test this
|
||||
*/
|
||||
export function ToolbarActions({ dashboard }: Props) {
|
||||
const { isEditing, viewPanelScene, isDirty, uid, meta, editview } = dashboard.useState();
|
||||
const canSaveAs = contextSrv.hasEditPermissionInFolders;
|
||||
const toolbarActions: ToolbarAction[] = [];
|
||||
const buttonWithExtraMargin = useStyles2(getStyles);
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'icon-actions',
|
||||
condition: uid && !editview && Boolean(meta.canStar),
|
||||
render: () => {
|
||||
let desc = meta.isStarred
|
||||
? t('dashboard.toolbar.unmark-favorite', 'Unmark as favorite')
|
||||
: t('dashboard.toolbar.mark-favorite', 'Mark as favorite');
|
||||
|
||||
toolbarActions.push(
|
||||
<DashNavButton
|
||||
key="star-dashboard-button"
|
||||
return (
|
||||
<ToolbarButton
|
||||
tooltip={desc}
|
||||
icon={meta.isStarred ? 'favorite' : 'star'}
|
||||
iconType={meta.isStarred ? 'mono' : 'default'}
|
||||
iconSize="lg"
|
||||
icon={
|
||||
<Icon name={meta.isStarred ? 'favorite' : 'star'} size="lg" type={meta.isStarred ? 'mono' : 'default'} />
|
||||
}
|
||||
key="star-dashboard-button"
|
||||
onClick={() => {
|
||||
DashboardInteractions.toolbarFavoritesClick();
|
||||
dashboard.onStarDashboard();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
toolbarActions.push(
|
||||
<DashNavButton
|
||||
key="share-dashboard-button"
|
||||
tooltip={t('dashboard.toolbar.share', 'Share dashboard')}
|
||||
icon="share-alt"
|
||||
iconSize="lg"
|
||||
onClick={() => {
|
||||
DashboardInteractions.toolbarShareClick();
|
||||
dashboard.showModal(new ShareModal({ dashboardRef: dashboard.getRef() }));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
toolbarActions.push(
|
||||
<DashNavButton
|
||||
toolbarActions.push({
|
||||
group: 'icon-actions',
|
||||
condition: uid && !editview,
|
||||
render: () => (
|
||||
<ToolbarButton
|
||||
key="view-in-old-dashboard-button"
|
||||
tooltip={'View as dashboard'}
|
||||
tooltip={'Switch to old dashboard page'}
|
||||
icon="apps"
|
||||
onClick={() => locationService.push(`/d/${uid}`)}
|
||||
/>
|
||||
);
|
||||
),
|
||||
});
|
||||
|
||||
if (dynamicDashNavActions.left.length > 0) {
|
||||
dynamicDashNavActions.left.map((action, index) => {
|
||||
const props = { dashboard: getDashboardSrv().getCurrent()! };
|
||||
if (action.show(props)) {
|
||||
const Component = action.component;
|
||||
const element = <Component dashboard={_legacyDashboardModel} />;
|
||||
typeof action.index === 'number'
|
||||
? toolbarActions.splice(action.index, 0, element)
|
||||
: toolbarActions.push(element);
|
||||
toolbarActions.push({
|
||||
group: 'icon-actions',
|
||||
condition: true,
|
||||
render: () => <Component {...props} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toolbarActions.push(<NavToolbarSeparator leftActionsSeparator key="separator" />);
|
||||
|
||||
if (dynamicDashNavActions.right.length > 0) {
|
||||
dynamicDashNavActions.right.map((action, index) => {
|
||||
const Component = action.component;
|
||||
const element = <Component dashboard={_legacyDashboardModel} key={`button-custom-${index}`} />;
|
||||
typeof action.index === 'number'
|
||||
? rightToolbarActions.splice(action.index, 0, element)
|
||||
: rightToolbarActions.push(element);
|
||||
});
|
||||
|
||||
toolbarActions.push(...rightToolbarActions);
|
||||
}
|
||||
|
||||
if (viewPanelScene) {
|
||||
toolbarActions.push(
|
||||
toolbarActions.push({
|
||||
group: 'back-button',
|
||||
condition: Boolean(viewPanelScene),
|
||||
render: () => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
locationService.partial({ viewPanel: null });
|
||||
}}
|
||||
tooltip=""
|
||||
key="back"
|
||||
variant="primary"
|
||||
fill="text"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon="arrow-left"
|
||||
>
|
||||
Back to dashboard
|
||||
</Button>
|
||||
);
|
||||
),
|
||||
});
|
||||
|
||||
return <AppChromeUpdate actions={toolbarActions} />;
|
||||
}
|
||||
toolbarActions.push({
|
||||
group: 'back-button',
|
||||
condition: Boolean(editview),
|
||||
render: () => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
locationService.partial({ editview: null });
|
||||
}}
|
||||
tooltip=""
|
||||
key="back"
|
||||
fill="text"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon="arrow-left"
|
||||
>
|
||||
Back to dashboard
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
|
||||
if (!isEditing) {
|
||||
if (dashboard.canEditDashboard()) {
|
||||
toolbarActions.push(
|
||||
toolbarActions.push({
|
||||
group: 'main-buttons',
|
||||
condition: uid && !isEditing,
|
||||
render: () => (
|
||||
<Button
|
||||
key="share-dashboard-button"
|
||||
tooltip={t('dashboard.toolbar.share', 'Share dashboard')}
|
||||
size="sm"
|
||||
className={buttonWithExtraMargin}
|
||||
fill="outline"
|
||||
onClick={() => {
|
||||
DashboardInteractions.toolbarShareClick();
|
||||
dashboard.showModal(new ShareModal({ dashboardRef: dashboard.getRef() }));
|
||||
}}
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'main-buttons',
|
||||
condition: !isEditing && dashboard.canEditDashboard() && !viewPanelScene,
|
||||
render: () => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
dashboard.onEnterEditMode();
|
||||
}}
|
||||
tooltip="Enter edit mode"
|
||||
key="edit"
|
||||
className={buttonWithExtraMargin}
|
||||
variant="primary"
|
||||
icon="pen"
|
||||
fill="text"
|
||||
size="sm"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (dashboard.canEditDashboard()) {
|
||||
if (!dashboard.state.meta.isNew) {
|
||||
toolbarActions.push(
|
||||
),
|
||||
});
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'settings',
|
||||
condition: isEditing && dashboard.canEditDashboard() && !viewPanelScene && !editview,
|
||||
render: () => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
dashboard.openSaveDrawer({ saveAsCopy: true });
|
||||
dashboard.onOpenSettings();
|
||||
}}
|
||||
tooltip="Dashboard settings"
|
||||
fill="text"
|
||||
size="sm"
|
||||
tooltip="Save as copy"
|
||||
fill="text"
|
||||
key="save-as"
|
||||
key="settings"
|
||||
variant="secondary"
|
||||
>
|
||||
Save as
|
||||
Settings
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (dashboard.canDiscard()) {
|
||||
toolbarActions.push(
|
||||
),
|
||||
});
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'main-buttons',
|
||||
condition: isEditing && !editview && !meta.isNew,
|
||||
render: () => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
dashboard.onDiscard();
|
||||
}}
|
||||
tooltip="Discard changes"
|
||||
fill="text"
|
||||
onClick={() => dashboard.exitEditMode({ skipConfirm: false })}
|
||||
tooltip="Exits edit mode and discards unsaved changes"
|
||||
size="sm"
|
||||
key="discard"
|
||||
variant="destructive"
|
||||
fill="text"
|
||||
variant="primary"
|
||||
>
|
||||
Discard
|
||||
Exit edit
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'main-buttons',
|
||||
condition: isEditing && (meta.canSave || canSaveAs),
|
||||
render: () => {
|
||||
// if we only can save
|
||||
if (meta.isNew) {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
DashboardInteractions.toolbarSaveClick();
|
||||
dashboard.openSaveDrawer({});
|
||||
}}
|
||||
className={buttonWithExtraMargin}
|
||||
tooltip="Save changes"
|
||||
key="save"
|
||||
size="sm"
|
||||
variant={'primary'}
|
||||
>
|
||||
Save dashboard
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
toolbarActions.push(
|
||||
|
||||
// If we only can save as copy
|
||||
if (canSaveAs && !meta.canSave) {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
DashboardInteractions.toolbarSaveClick();
|
||||
dashboard.openSaveDrawer({ saveAsCopy: true });
|
||||
}}
|
||||
className={buttonWithExtraMargin}
|
||||
tooltip="Save as copy"
|
||||
key="save"
|
||||
size="sm"
|
||||
variant={isDirty ? 'primary' : 'secondary'}
|
||||
>
|
||||
Save as copy
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// If we can do both save and save as copy we show a button group with dropdown menu
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
label="Save"
|
||||
icon="save"
|
||||
onClick={() => {
|
||||
DashboardInteractions.toolbarSaveClick();
|
||||
dashboard.openSaveDrawer({});
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
label="Save as copy"
|
||||
icon="copy"
|
||||
onClick={() => {
|
||||
DashboardInteractions.toolbarSaveAsClick();
|
||||
dashboard.openSaveDrawer({ saveAsCopy: true });
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<ButtonGroup className={buttonWithExtraMargin} key="save">
|
||||
<Button
|
||||
onClick={() => {
|
||||
DashboardInteractions.toolbarSaveClick();
|
||||
dashboard.openSaveDrawer({});
|
||||
}}
|
||||
tooltip="Save changes"
|
||||
key="save"
|
||||
size="sm"
|
||||
variant={isDirty ? 'primary' : 'secondary'}
|
||||
>
|
||||
Save
|
||||
Save dashboard
|
||||
</Button>
|
||||
<Dropdown overlay={menu}>
|
||||
<Button icon="angle-down" variant={isDirty ? 'primary' : 'secondary'} size="sm" />
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const actionElements: React.ReactNode[] = [];
|
||||
let lastGroup = '';
|
||||
|
||||
for (const action of toolbarActions) {
|
||||
if (!action.condition) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return <AppChromeUpdate actions={toolbarActions} />;
|
||||
});
|
||||
if (lastGroup && lastGroup !== action.group) {
|
||||
lastGroup && actionElements.push(<NavToolbarSeparator key={`${action.group}-separator`} />);
|
||||
}
|
||||
|
||||
NavToolbarActions.displayName = 'NavToolbarActions';
|
||||
actionElements.push(action.render());
|
||||
lastGroup = action.group;
|
||||
}
|
||||
|
||||
return actionElements;
|
||||
}
|
||||
|
||||
interface ToolbarAction {
|
||||
group: string;
|
||||
condition?: boolean | string;
|
||||
render: () => React.ReactNode;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return css({ margin: theme.spacing(0, 0.5) });
|
||||
}
|
||||
|
@ -135,7 +135,9 @@ export const DashboardInteractions = {
|
||||
toolbarSaveClick: () => {
|
||||
reportDashboardInteraction('toolbar_actions_clicked', { item: 'save' });
|
||||
},
|
||||
|
||||
toolbarSaveAsClick: () => {
|
||||
reportDashboardInteraction('toolbar_actions_clicked', { item: 'save_as' });
|
||||
},
|
||||
toolbarAddClick: () => {
|
||||
reportDashboardInteraction('toolbar_actions_clicked', { item: 'add' });
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user