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:
Torkel Ödegaard 2024-02-05 16:08:12 +01:00 committed by GitHub
parent 9b18a4d45e
commit 61c7fcc270
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 370 additions and 135 deletions

View File

@ -117,7 +117,7 @@ const getStyles = (theme: GrafanaTheme2, overflowButtonOrder: number, alignment:
alignItems: 'center', alignItems: 'center',
backgroundColor: theme.colors.background.primary, backgroundColor: theme.colors.background.primary,
borderRadius: theme.shape.radius.default, borderRadius: theme.shape.radius.default,
boxShadow: theme.shadows.z3, boxShadow: theme.shadows.z2,
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: theme.spacing(1), gap: theme.spacing(1),
@ -128,7 +128,7 @@ const getStyles = (theme: GrafanaTheme2, overflowButtonOrder: number, alignment:
right: 0, right: 0,
top: '100%', top: '100%',
width: 'max-content', width: 'max-content',
zIndex: theme.zIndex.sidemenu, zIndex: theme.zIndex.dropdown,
}), }),
container: css({ container: css({
alignItems: 'center', alignItems: 'center',

View File

@ -62,7 +62,6 @@ export function NavToolbar({
<Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} /> <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
<div className={styles.actions}> <div className={styles.actions}>
{actions} {actions}
{actions && <NavToolbarSeparator />}
{searchBarHidden && ( {searchBarHidden && (
<ToolbarButton <ToolbarButton
onClick={onToggleKioskMode} onClick={onToggleKioskMode}
@ -71,6 +70,7 @@ export function NavToolbar({
icon="monitor" icon="monitor"
/> />
)} )}
{actions && <NavToolbarSeparator />}
<ToolbarButton <ToolbarButton
onClick={onToggleSearchBar} onClick={onToggleSearchBar}
narrow narrow

View File

@ -12,7 +12,7 @@ export function initAlerting() {
addCustomRightAction({ addCustomRightAction({
show: () => config.unifiedAlertingEnabled, show: () => config.unifiedAlertingEnabled,
component: ({ dashboard }) => ( component: ({ dashboard }) => (
<React.Suspense fallback={null}> <React.Suspense fallback={null} key="alert-rules-button">
{dashboard && <AlertRulesToolbarButton dashboardUid={dashboard.uid} />} {dashboard && <AlertRulesToolbarButton dashboardUid={dashboard.uid} />}
</React.Suspense> </React.Suspense>
), ),

View File

@ -1,9 +1,7 @@
import React from 'react'; import React from 'react';
import { SceneObjectState, SceneObject, SceneObjectBase, SceneComponentProps } from '@grafana/scenes'; import { SceneObjectState, SceneObject, SceneObjectBase, SceneComponentProps } from '@grafana/scenes';
import { Box, Stack, ToolbarButton } from '@grafana/ui'; import { Box, Stack } from '@grafana/ui';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardLinksControls } from './DashboardLinksControls'; import { DashboardLinksControls } from './DashboardLinksControls';
@ -18,9 +16,7 @@ export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
} }
function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardControls>) { function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardControls>) {
const dashboard = getDashboardSceneFor(model);
const { variableControls, linkControls, timeControls, hideTimeControls } = model.useState(); const { variableControls, linkControls, timeControls, hideTimeControls } = model.useState();
const { isEditing } = dashboard.useState();
return ( return (
<Stack <Stack
@ -38,9 +34,6 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
<linkControls.Component model={linkControls} /> <linkControls.Component model={linkControls} />
</Stack> </Stack>
<Stack justifyContent={'flex-end'}> <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} />)} {!hideTimeControls && timeControls.map((c) => <c.Component model={c} key={c.state.key} />)}
</Stack> </Stack>
</Stack> </Stack>

View File

@ -57,7 +57,7 @@ describe('DashboardScene', () => {
expect(scene.state.isDirty).toBe(true); 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; const gridItem2 = sceneGraph.findObject(scene, (p) => p.state.key === 'griditem-1') as SceneGridItem;
expect(gridItem2.state.x).toBe(0); expect(gridItem2.state.x).toBe(0);
}); });
@ -77,7 +77,7 @@ describe('DashboardScene', () => {
expect(scene.state.isDirty).toBe(true); expect(scene.state.isDirty).toBe(true);
scene.onDiscard(); scene.exitEditMode({ skipConfirm: true });
expect(scene.state[prop]).toEqual(prevState); expect(scene.state[prop]).toEqual(prevState);
} }
); );
@ -89,7 +89,7 @@ describe('DashboardScene', () => {
expect(scene.state.isDirty).toBe(true); expect(scene.state.isDirty).toBe(true);
scene.onDiscard(); scene.exitEditMode({ skipConfirm: true });
expect(dashboardSceneGraph.getRefreshPicker(scene)!.state.intervals).toEqual(prevState); expect(dashboardSceneGraph.getRefreshPicker(scene)!.state.intervals).toEqual(prevState);
}); });
@ -100,7 +100,7 @@ describe('DashboardScene', () => {
expect(scene.state.isDirty).toBe(true); expect(scene.state.isDirty).toBe(true);
scene.onDiscard(); scene.exitEditMode({ skipConfirm: true });
expect(dashboardSceneGraph.getDashboardControls(scene)!.state.hideTimeControls).toEqual(prevState); expect(dashboardSceneGraph.getDashboardControls(scene)!.state.hideTimeControls).toEqual(prevState);
}); });
@ -111,7 +111,7 @@ describe('DashboardScene', () => {
expect(scene.state.isDirty).toBe(true); expect(scene.state.isDirty).toBe(true);
scene.onDiscard(); scene.exitEditMode({ skipConfirm: true });
expect(sceneGraph.getTimeRange(scene)!.state.timeZone).toBe(prevState); expect(sceneGraph.getTimeRange(scene)!.state.timeZone).toBe(prevState);
}); });
}); });

View File

@ -26,6 +26,7 @@ import { DashboardModel } from 'app/features/dashboard/state';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { VariablesChanged } from 'app/features/variables/types'; import { VariablesChanged } from 'app/features/variables/types';
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events';
import { PanelEditor } from '../panel-edit/PanelEditor'; import { PanelEditor } from '../panel-edit/PanelEditor';
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer'; 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()) { if (!this.canDiscard()) {
console.error('Trying to discard back to a state that does not exist, initialState undefined'); console.error('Trying to discard back to a state that does not exist, initialState undefined');
return; 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 // No need to listen to changes anymore
this.stopTrackingChanges(); this.stopTrackingChanges();
// Stop url sync before updating url // Stop url sync before updating url
@ -244,7 +262,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this.startUrlSync(); this.startUrlSync();
// Disable grid dragging // Disable grid dragging
this.propagateEditModeChange(); this.propagateEditModeChange();
}; }
public canDiscard() { public canDiscard() {
return this._initialState !== undefined; return this._initialState !== undefined;
@ -266,7 +284,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
newState.version = versionRsp.version; newState.version = versionRsp.version;
this._initialState = newState; this._initialState = newState;
this.onDiscard(); this.exitEditMode({ skipConfirm: false });
return true; return true;
}; };

View File

@ -20,6 +20,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
const pageNav = model.getPageNav(location, navIndex); const pageNav = model.getPageNav(location, navIndex);
const bodyToRender = model.getBodyToRender(); const bodyToRender = model.getBodyToRender();
const navModel = getNavModel(navIndex, 'dashboards/browse'); const navModel = getNavModel(navIndex, 'dashboards/browse');
const showDebugger = location.search.includes('scene-debugger');
if (editview) { if (editview) {
return ( return (
@ -43,7 +44,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
{controls.map((control) => ( {controls.map((control) => (
<control.Component key={control.state.key} model={control} /> <control.Component key={control.state.key} model={control} />
))} ))}
<SceneDebugger scene={model} key={'scene-debugger'} /> {showDebugger && <SceneDebugger scene={model} key={'scene-debugger'} />}
</div> </div>
)} )}
<div className={cx(styles.body)}> <div className={cx(styles.body)}>
@ -83,7 +84,7 @@ function getStyles(theme: GrafanaTheme2) {
position: 'sticky', position: 'sticky',
top: 0, top: 0,
background: theme.colors.background.canvas, background: theme.colors.background.canvas,
zIndex: theme.zIndex.navbarFixed, zIndex: theme.zIndex.activePanel,
padding: theme.spacing(2, 0), padding: theme.spacing(2, 0),
}), }),
}; };

View File

@ -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 };
}

View File

@ -1,11 +1,13 @@
import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { locationService } from '@grafana/runtime'; 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 { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator'; import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
import { contextSrv } from 'app/core/core';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { ShareModal } from '../sharing/ShareModal'; import { ShareModal } from '../sharing/ShareModal';
@ -19,164 +21,300 @@ interface Props {
} }
export const NavToolbarActions = React.memo<Props>(({ dashboard }) => { export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
const { actions = [], isEditing, viewPanelScene, isDirty, uid, meta, editview } = dashboard.useState(); const actions = (
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />); <ToolbarButtonRow alignment="right">
const rightToolbarActions: JSX.Element[] = []; <ToolbarActions dashboard={dashboard} />
const _legacyDashboardModel = getDashboardSrv().getCurrent(); </ToolbarButtonRow>
);
if (uid && !editview) { return <AppChromeUpdate actions={actions} />;
if (meta.canStar) { });
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 let desc = meta.isStarred
? t('dashboard.toolbar.unmark-favorite', 'Unmark as favorite') ? t('dashboard.toolbar.unmark-favorite', 'Unmark as favorite')
: t('dashboard.toolbar.mark-favorite', 'Mark as favorite'); : t('dashboard.toolbar.mark-favorite', 'Mark as favorite');
return (
toolbarActions.push( <ToolbarButton
<DashNavButton
key="star-dashboard-button"
tooltip={desc} tooltip={desc}
icon={meta.isStarred ? 'favorite' : 'star'} icon={
iconType={meta.isStarred ? 'mono' : 'default'} <Icon name={meta.isStarred ? 'favorite' : 'star'} size="lg" type={meta.isStarred ? 'mono' : 'default'} />
iconSize="lg" }
key="star-dashboard-button"
onClick={() => { onClick={() => {
DashboardInteractions.toolbarFavoritesClick(); DashboardInteractions.toolbarFavoritesClick();
dashboard.onStarDashboard(); 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( toolbarActions.push({
<DashNavButton group: 'icon-actions',
condition: uid && !editview,
render: () => (
<ToolbarButton
key="view-in-old-dashboard-button" key="view-in-old-dashboard-button"
tooltip={'View as dashboard'} tooltip={'Switch to old dashboard page'}
icon="apps" icon="apps"
onClick={() => locationService.push(`/d/${uid}`)} onClick={() => locationService.push(`/d/${uid}`)}
/> />
); ),
if (dynamicDashNavActions.left.length > 0) { });
dynamicDashNavActions.left.map((action, index) => {
if (dynamicDashNavActions.left.length > 0) {
dynamicDashNavActions.left.map((action, index) => {
const props = { dashboard: getDashboardSrv().getCurrent()! };
if (action.show(props)) {
const Component = action.component; const Component = action.component;
const element = <Component dashboard={_legacyDashboardModel} />; toolbarActions.push({
typeof action.index === 'number' group: 'icon-actions',
? toolbarActions.splice(action.index, 0, element) condition: true,
: toolbarActions.push(element); 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 <Button
onClick={() => { onClick={() => {
locationService.partial({ viewPanel: null }); locationService.partial({ viewPanel: null });
}} }}
tooltip="" tooltip=""
key="back" key="back"
variant="primary" variant="secondary"
fill="text" size="sm"
icon="arrow-left"
> >
Back to dashboard Back to dashboard
</Button> </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) { toolbarActions.push({
if (dashboard.canEditDashboard()) { group: 'main-buttons',
toolbarActions.push( condition: uid && !isEditing,
<Button render: () => (
onClick={() => { <Button
dashboard.onEnterEditMode(); key="share-dashboard-button"
}} tooltip={t('dashboard.toolbar.share', 'Share dashboard')}
tooltip="Enter edit mode" size="sm"
key="edit" className={buttonWithExtraMargin}
variant="primary" fill="outline"
icon="pen" onClick={() => {
fill="text" DashboardInteractions.toolbarShareClick();
size="sm" dashboard.showModal(new ShareModal({ dashboardRef: dashboard.getRef() }));
> }}
Edit >
</Button> Share
); </Button>
} ),
} else { });
if (dashboard.canEditDashboard()) {
if (!dashboard.state.meta.isNew) { toolbarActions.push({
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"
size="sm"
>
Edit
</Button>
),
});
toolbarActions.push({
group: 'settings',
condition: isEditing && dashboard.canEditDashboard() && !viewPanelScene && !editview,
render: () => (
<Button
onClick={() => {
dashboard.onOpenSettings();
}}
tooltip="Dashboard settings"
fill="text"
size="sm"
key="settings"
variant="secondary"
>
Settings
</Button>
),
});
toolbarActions.push({
group: 'main-buttons',
condition: isEditing && !editview && !meta.isNew,
render: () => (
<Button
onClick={() => dashboard.exitEditMode({ skipConfirm: false })}
tooltip="Exits edit mode and discards unsaved changes"
size="sm"
key="discard"
fill="text"
variant="primary"
>
Exit edit
</Button>
),
});
toolbarActions.push({
group: 'main-buttons',
condition: isEditing && (meta.canSave || canSaveAs),
render: () => {
// if we only can save
if (meta.isNew) {
return (
<Button <Button
onClick={() => { onClick={() => {
DashboardInteractions.toolbarSaveClick();
dashboard.openSaveDrawer({});
}}
className={buttonWithExtraMargin}
tooltip="Save changes"
key="save"
size="sm"
variant={'primary'}
>
Save dashboard
</Button>
);
}
// If we only can save as copy
if (canSaveAs && !meta.canSave) {
return (
<Button
onClick={() => {
DashboardInteractions.toolbarSaveClick();
dashboard.openSaveDrawer({ saveAsCopy: true }); dashboard.openSaveDrawer({ saveAsCopy: true });
}} }}
size="sm" className={buttonWithExtraMargin}
tooltip="Save as copy" tooltip="Save as copy"
fill="text" key="save"
key="save-as" size="sm"
variant={isDirty ? 'primary' : 'secondary'}
> >
Save as Save as copy
</Button> </Button>
); );
} }
if (dashboard.canDiscard()) {
toolbarActions.push( // 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 <Button
onClick={() => { onClick={() => {
dashboard.onDiscard(); DashboardInteractions.toolbarSaveClick();
dashboard.openSaveDrawer({});
}} }}
tooltip="Discard changes" tooltip="Save changes"
fill="text"
size="sm" size="sm"
key="discard" variant={isDirty ? 'primary' : 'secondary'}
variant="destructive"
> >
Discard Save dashboard
</Button> </Button>
); <Dropdown overlay={menu}>
} <Button icon="angle-down" variant={isDirty ? 'primary' : 'secondary'} size="sm" />
toolbarActions.push( </Dropdown>
<Button </ButtonGroup>
onClick={() => {
DashboardInteractions.toolbarSaveClick();
dashboard.openSaveDrawer({});
}}
tooltip="Save changes"
key="save"
size="sm"
variant={isDirty ? 'primary' : 'secondary'}
>
Save
</Button>
); );
},
});
const actionElements: React.ReactNode[] = [];
let lastGroup = '';
for (const action of toolbarActions) {
if (!action.condition) {
continue;
} }
if (lastGroup && lastGroup !== action.group) {
lastGroup && actionElements.push(<NavToolbarSeparator key={`${action.group}-separator`} />);
}
actionElements.push(action.render());
lastGroup = action.group;
} }
return <AppChromeUpdate actions={toolbarActions} />; return actionElements;
}); }
NavToolbarActions.displayName = 'NavToolbarActions'; interface ToolbarAction {
group: string;
condition?: boolean | string;
render: () => React.ReactNode;
}
function getStyles(theme: GrafanaTheme2) {
return css({ margin: theme.spacing(0, 0.5) });
}

View File

@ -135,7 +135,9 @@ export const DashboardInteractions = {
toolbarSaveClick: () => { toolbarSaveClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'save' }); reportDashboardInteraction('toolbar_actions_clicked', { item: 'save' });
}, },
toolbarSaveAsClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'save_as' });
},
toolbarAddClick: () => { toolbarAddClick: () => {
reportDashboardInteraction('toolbar_actions_clicked', { item: 'add' }); reportDashboardInteraction('toolbar_actions_clicked', { item: 'add' });
}, },