Dynamic Dashboards: Implement new toolbar (#102195)

pull/102391/head^2
Bogdan Matei 4 months ago committed by GitHub
parent 47b94cf17b
commit d3832c7f8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 233
      public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
  2. 20
      public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx
  3. 79
      public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx
  4. 154
      public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
  5. 19
      public/app/features/dashboard-scene/scene/new-toolbar/ToolbarActionsNew.tsx
  6. 22
      public/app/features/dashboard-scene/scene/new-toolbar/actions/BackToDashboardButton.tsx
  7. 14
      public/app/features/dashboard-scene/scene/new-toolbar/actions/DashboardSettingsButton.tsx
  8. 18
      public/app/features/dashboard-scene/scene/new-toolbar/actions/DiscardLibraryPanelButton.tsx
  9. 54
      public/app/features/dashboard-scene/scene/new-toolbar/actions/DiscardPanelButton.tsx
  10. 28
      public/app/features/dashboard-scene/scene/new-toolbar/actions/EditDashboardSwitch.tsx
  11. 12
      public/app/features/dashboard-scene/scene/new-toolbar/actions/EditSchemaV2Button.tsx
  12. 33
      public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx
  13. 22
      public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.tsx
  14. 31
      public/app/features/dashboard-scene/scene/new-toolbar/actions/ManagedDashboardBadge.tsx
  15. 6
      public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx
  16. 16
      public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListNextButton.tsx
  17. 15
      public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListPreviousButton.tsx
  18. 15
      public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListStopButton.tsx
  19. 28
      public/app/features/dashboard-scene/scene/new-toolbar/actions/PublicDashboardBadge.tsx
  20. 80
      public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx
  21. 18
      public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveLibraryPanelButton.tsx
  22. 40
      public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx
  23. 100
      public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareExportDashboardButton.tsx
  24. 28
      public/app/features/dashboard-scene/scene/new-toolbar/actions/StarButton.tsx
  25. 119
      public/app/features/dashboard-scene/scene/new-toolbar/actions/ToolbarSwitch.tsx
  26. 18
      public/app/features/dashboard-scene/scene/new-toolbar/actions/UnlinkLibraryPanelButton.tsx
  27. 14
      public/app/features/dashboard-scene/scene/new-toolbar/types.ts
  28. 62
      public/app/features/dashboard-scene/scene/new-toolbar/utils.tsx
  29. 6
      public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.tsx
  30. 62
      public/locales/en-US/grafana.json

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { memo, ReactNode, useEffect, useId, useState } from 'react';
import { memo, ReactNode, useEffect, useState } from 'react';
import { GrafanaTheme2, store } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@ -10,10 +10,8 @@ import {
ButtonGroup,
Dropdown,
Icon,
InlineLabel,
Menu,
Stack,
Switch,
ToolbarButton,
ToolbarButtonRow,
useStyles2,
@ -39,15 +37,20 @@ import { isLibraryPanel } from '../utils/utils';
import { DashboardScene } from './DashboardScene';
import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton';
import ManagedDashboardNavBarBadge from './ManagedDashboardNavBarBadge';
import { ToolbarActionsNew } from './new-toolbar/ToolbarActionsNew';
interface Props {
dashboard: DashboardScene;
}
export const NavToolbarActions = memo<Props>(({ dashboard }) => {
const id = useId();
const hasNewToolbar = config.featureToggles.dashboardNewLayouts && config.featureToggles.newDashboardSharingComponent;
const actions = <ToolbarActions dashboard={dashboard} key={id} />;
const actions = hasNewToolbar ? (
<ToolbarActionsNew dashboard={dashboard} key={`${dashboard.state.key}-toolbar-actions-new`} />
) : (
<ToolbarActions dashboard={dashboard} key={`${dashboard.state.key}-toolbar-actions`} />
);
return <AppChromeUpdate actions={actions} />;
});
@ -57,8 +60,7 @@ NavToolbarActions.displayName = 'NavToolbarActions';
* This part is split into a separate component to help test this
*/
export function ToolbarActions({ dashboard }: Props) {
const { isEditing, showHiddenElements, viewPanelScene, isDirty, uid, meta, editview, editPanel, editable } =
dashboard.useState();
const { isEditing, viewPanelScene, isDirty, uid, meta, editview, editPanel, editable } = dashboard.useState();
const { isPlaying } = playlistSrv.useState();
const [isAddPanelMenuOpen, setIsAddPanelMenuOpen] = useState(false);
@ -77,7 +79,6 @@ export function ToolbarActions({ dashboard }: Props) {
// Means we are not in settings view, fullscreen panel or edit panel
const isShowingDashboard = !editview && !isViewingPanel && !isEditingPanel;
const isEditingAndShowingDashboard = isEditing && isShowingDashboard;
const dashboardNewLayouts = config.featureToggles.dashboardNewLayouts;
const folderRepo = useSelector((state) => selectFolderRepository(state, meta.folderUid));
const isManaged = Boolean(dashboard.isManagedRepository() || folderRepo);
@ -168,97 +169,75 @@ export function ToolbarActions({ dashboard }: Props) {
addDynamicActions(toolbarActions, dynamicDashNavActions.right, 'icon-actions');
}
if (dashboardNewLayouts) {
leftActions.push({
group: 'hidden-elements',
condition: isEditingAndShowingDashboard,
render: () => (
<InlineLabel key="toggle-hidden-elements" transparent={true} className={styles.hiddenElementsContainer}>
<Switch
value={showHiddenElements}
onChange={(evt) => {
evt.stopPropagation();
dashboard.onToggleHiddenElements();
}}
data-testid={selectors.components.PageToolbar.itemButton('toggle_hidden_elements')}
/>
<span>
<Trans i18nKey="dashboard.toolbar.show-hidden-elements">Show hidden</Trans>
</span>
</InlineLabel>
),
});
} else {
toolbarActions.push({
group: 'add-panel',
condition: isEditingAndShowingDashboard,
render: () => (
<Dropdown
key="add-panel-dropdown"
onVisibleChange={(isOpen) => {
setIsAddPanelMenuOpen(isOpen);
DashboardInteractions.toolbarAddClick();
}}
overlay={() => (
<Menu>
<Menu.Item
key="add-visualization"
testId={selectors.pages.AddDashboard.itemButton('Add new visualization menu item')}
label={t('dashboard.add-menu.visualization', 'Visualization')}
onClick={() => {
const vizPanel = dashboard.onCreateNewPanel();
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' });
dashboard.setState({ editPanel: buildPanelEditScene(vizPanel, true) });
}}
/>
<Menu.Item
key="add-panel-lib"
testId={selectors.pages.AddDashboard.itemButton('Add new panel from panel library menu item')}
label={t('dashboard.add-menu.import', 'Import from library')}
onClick={() => {
dashboard.onShowAddLibraryPanelDrawer();
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' });
}}
disabled={dashboard.isManagedRepository()}
/>
<Menu.Item
key="add-row"
testId={selectors.pages.AddDashboard.itemButton('Add new row menu item')}
label={t('dashboard.add-menu.row', 'Row')}
onClick={() => {
dashboard.onCreateNewRow();
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' });
}}
/>
<Menu.Item
key="paste-panel"
disabled={!hasCopiedPanel}
testId={selectors.pages.AddDashboard.itemButton('Add new panel from clipboard menu item')}
label={t('dashboard.add-menu.paste-panel', 'Paste panel')}
onClick={() => {
dashboard.pastePanel();
DashboardInteractions.toolbarAddButtonClicked({ item: 'paste_panel' });
}}
/>
</Menu>
)}
placement="bottom"
offset={[0, 6]}
toolbarActions.push({
group: 'add-panel',
condition: isEditingAndShowingDashboard,
render: () => (
<Dropdown
key="add-panel-dropdown"
onVisibleChange={(isOpen) => {
setIsAddPanelMenuOpen(isOpen);
DashboardInteractions.toolbarAddClick();
}}
overlay={() => (
<Menu>
<Menu.Item
key="add-visualization"
testId={selectors.pages.AddDashboard.itemButton('Add new visualization menu item')}
label={t('dashboard.add-menu.visualization', 'Visualization')}
onClick={() => {
const vizPanel = dashboard.onCreateNewPanel();
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' });
dashboard.setState({ editPanel: buildPanelEditScene(vizPanel, true) });
}}
/>
<Menu.Item
key="add-panel-lib"
testId={selectors.pages.AddDashboard.itemButton('Add new panel from panel library menu item')}
label={t('dashboard.add-menu.import', 'Import from library')}
onClick={() => {
dashboard.onShowAddLibraryPanelDrawer();
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' });
}}
disabled={dashboard.isManagedRepository()}
/>
<Menu.Item
key="add-row"
testId={selectors.pages.AddDashboard.itemButton('Add new row menu item')}
label={t('dashboard.add-menu.row', 'Row')}
onClick={() => {
dashboard.onCreateNewRow();
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' });
}}
/>
<Menu.Item
key="paste-panel"
disabled={!hasCopiedPanel}
testId={selectors.pages.AddDashboard.itemButton('Add new panel from clipboard menu item')}
label={t('dashboard.add-menu.paste-panel', 'Paste panel')}
onClick={() => {
dashboard.pastePanel();
DashboardInteractions.toolbarAddButtonClicked({ item: 'paste_panel' });
}}
/>
</Menu>
)}
placement="bottom"
offset={[0, 6]}
>
<Button
key="add-panel-button"
variant="primary"
size="sm"
fill="outline"
data-testid={selectors.components.PageToolbar.itemButton('Add button')}
>
<Button
key="add-panel-button"
variant="primary"
size="sm"
fill="outline"
data-testid={selectors.components.PageToolbar.itemButton('Add button')}
>
<Trans i18nKey="dashboard.toolbar.add">Add</Trans>
<Icon name={isAddPanelMenuOpen ? 'angle-up' : 'angle-down'} size="lg" />
</Button>
</Dropdown>
),
});
}
<Trans i18nKey="dashboard.toolbar.add">Add</Trans>
<Icon name={isAddPanelMenuOpen ? 'angle-up' : 'angle-down'} size="lg" />
</Button>
</Dropdown>
),
});
toolbarActions.push({
group: 'playlist-actions',
@ -419,27 +398,25 @@ export function ToolbarActions({ dashboard }: Props) {
render: () => <ShareButton key="new-share-dashboard-button" dashboard={dashboard} />,
});
if (!dashboardNewLayouts) {
toolbarActions.push({
group: 'settings',
condition: isEditing && dashboard.canEditDashboard() && isShowingDashboard,
render: () => (
<Button
onClick={() => {
dashboard.onOpenSettings();
}}
tooltip={t('dashboard.toolbar.dashboard-settings.tooltip', 'Dashboard settings')}
fill="text"
size="sm"
key="settings"
variant="secondary"
data-testid={selectors.components.NavToolbar.editDashboard.settingsButton}
>
<Trans i18nKey="dashboard.toolbar.dashboard-settings.label">Settings</Trans>
</Button>
),
});
}
toolbarActions.push({
group: 'settings',
condition: isEditing && dashboard.canEditDashboard() && isShowingDashboard,
render: () => (
<Button
onClick={() => {
dashboard.onOpenSettings();
}}
tooltip={t('dashboard.toolbar.dashboard-settings.tooltip', 'Dashboard settings')}
fill="text"
size="sm"
key="settings"
variant="secondary"
data-testid={selectors.components.NavToolbar.editDashboard.settingsButton}
>
<Trans i18nKey="dashboard.toolbar.dashboard-settings.label">Settings</Trans>
</Button>
),
});
toolbarActions.push({
group: 'main-buttons',
@ -627,24 +604,6 @@ export function ToolbarActions({ dashboard }: Props) {
},
});
// Will open a schema v2 editor drawer. Only available with new dashboard layouts.
toolbarActions.push({
group: 'main-buttons',
condition: uid && dashboardNewLayouts,
render: () => {
return (
<ToolbarButton
tooltip={t('dashboard.toolbar.edit-dashboard-v2-schema', 'Edit dashboard v2 schema')}
icon={<Icon name="brackets-curly" size="lg" type="default" />}
key="schema-v2-button"
onClick={() => {
dashboard.openV2SchemaEditor();
}}
/>
);
},
});
const rightActionsElements: ReactNode[] = renderActionElements(toolbarActions);
const leftActionsElements: ReactNode[] = renderActionElements(leftActions);
const hasActionsToLeftAndRight = leftActionsElements.length > 0;

@ -77,18 +77,14 @@ export class ResponsiveGridLayoutManager
key: undefined,
layout: this.state.layout.clone({
key: undefined,
children: this.state.layout.state.children.map((child) => {
if (child instanceof ResponsiveGridItem) {
return child.clone({
key: undefined,
body: child.state.body.clone({
key: getVizPanelKeyForPanelId(dashboardSceneGraph.getNextPanelId(child.state.body)),
}),
});
}
return child.clone({ key: undefined });
}),
children: this.state.layout.state.children.map((child) =>
child.clone({
key: undefined,
body: child.state.body.clone({
key: getVizPanelKeyForPanelId(dashboardSceneGraph.getNextPanelId(child.state.body)),
}),
})
),
}),
});
}

@ -0,0 +1,79 @@
import { css } from '@emotion/css';
import { ToolbarButtonRow, useStyles2 } from '@grafana/ui';
import { dynamicDashNavActions } from '../../utils/registerDynamicDashNavAction';
import { DashboardScene } from '../DashboardScene';
import { ManagedDashboardBadge } from './actions/ManagedDashboardBadge';
import { OpenSnapshotOriginButton } from './actions/OpenSnapshotOriginButton';
import { PublicDashboardBadge } from './actions/PublicDashboardBadge';
import { StarButton } from './actions/StarButton';
import { getDynamicActions, renderActionElements, useIsManagedRepository } from './utils';
export const LeftActions = ({ dashboard }: { dashboard: DashboardScene }) => {
const styles = useStyles2(getStyles);
const { editview, editPanel, isEditing, uid, meta, viewPanelScene } = dashboard.useState();
const hasEditView = Boolean(editview);
const isViewingPanel = Boolean(viewPanelScene);
const isEditingDashboard = Boolean(isEditing);
const isEditingPanel = Boolean(editPanel);
const isPublicDashboard = Boolean(meta.publicDashboardEnabled);
const hasUid = Boolean(uid);
const canEdit = Boolean(meta.canEdit);
const canStar = Boolean(meta.canStar);
const isSnapshot = Boolean(meta.isSnapshot);
const isShowingDashboard = !hasEditView && !isViewingPanel && !isEditingPanel;
const isManagedRepository = useIsManagedRepository(dashboard);
const elements = renderActionElements(
[
// This adds the presence indicators in enterprise
...getDynamicActions(dynamicDashNavActions.left, 'left-dynamic', !isEditingPanel),
{
key: 'star-button',
component: StarButton,
group: 'actions',
condition: hasUid && canStar && isShowingDashboard && !isEditingDashboard,
},
{
key: 'public-dashboard-badge',
component: PublicDashboardBadge,
group: 'actions',
condition: isPublicDashboard && hasUid && canStar && isShowingDashboard && !isEditingDashboard,
},
{
key: 'managed-dashboard-badge',
component: ManagedDashboardBadge,
group: 'actions',
condition: isManagedRepository && canEdit,
},
{
key: 'open-snapshot-origin-button',
component: OpenSnapshotOriginButton,
group: 'actions',
condition: isSnapshot && !isEditingDashboard,
},
// This adds the presence indicators in enterprise
...getDynamicActions(dynamicDashNavActions.right, 'right-dynamic', !isEditingPanel && !isEditingDashboard),
],
dashboard
);
if (elements.length === 0) {
return null;
}
return (
<ToolbarButtonRow alignment="left" className={styles.container}>
{elements}
</ToolbarButtonRow>
);
};
const getStyles = () => ({
container: css({
flex: 1,
}),
});

@ -0,0 +1,154 @@
import { css } from '@emotion/css';
import { ToolbarButtonRow, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { isLibraryPanel } from '../../utils/utils';
import { DashboardScene } from '../DashboardScene';
import { BackToDashboardButton } from './actions/BackToDashboardButton';
import { DashboardSettingsButton } from './actions/DashboardSettingsButton';
import { DiscardLibraryPanelButton } from './actions/DiscardLibraryPanelButton';
import { DiscardPanelButton } from './actions/DiscardPanelButton';
import { EditDashboardSwitch } from './actions/EditDashboardSwitch';
import { EditSchemaV2Button } from './actions/EditSchemaV2Button';
import { ExportDashboardButton } from './actions/ExportDashboardButton';
import { MakeDashboardEditableButton } from './actions/MakeDashboardEditableButton';
import { PlayListNextButton } from './actions/PlayListNextButton';
import { PlayListPreviousButton } from './actions/PlayListPreviousButton';
import { PlayListStopButton } from './actions/PlayListStopButton';
import { SaveDashboard } from './actions/SaveDashboard';
import { SaveLibraryPanelButton } from './actions/SaveLibraryPanelButton';
import { ShareDashboardButton } from './actions/ShareDashboardButton';
import { UnlinkLibraryPanelButton } from './actions/UnlinkLibraryPanelButton';
import { renderActionElements } from './utils';
export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
const styles = useStyles2(getStyles);
const { editPanel, editable, editview, isEditing, uid, meta, viewPanelScene } = dashboard.useState();
const { isPlaying } = playlistSrv.useState();
const isEditable = Boolean(editable);
const canSave = Boolean(meta.canSave);
const hasUid = Boolean(uid);
const isEditingDashboard = Boolean(isEditing);
const hasEditView = Boolean(editview);
const isEditingPanel = Boolean(editPanel);
const isViewingPanel = Boolean(viewPanelScene);
const isEditingLibraryPanel = isEditingPanel && isLibraryPanel(editPanel!.state.panelRef.resolve());
const isShowingDashboard = !hasEditView && !isViewingPanel && !isEditingPanel;
const isEditingAndShowingDashboard = isEditingDashboard && isShowingDashboard;
const isSnapshot = Boolean(meta.isSnapshot);
const canSaveInFolder = contextSrv.hasEditPermissionInFolders;
const showPanelButtons = isEditingPanel && !hasEditView && !isViewingPanel;
const showPlayButtons = isPlaying && isShowingDashboard && !isEditingDashboard;
const showShareButton = hasUid && !isSnapshot && !isPlaying;
return (
<ToolbarButtonRow alignment="right" className={styles.container}>
{renderActionElements(
[
{
key: 'play-list-previous-button',
component: PlayListPreviousButton,
group: 'playlist',
condition: showPlayButtons,
},
{
key: 'play-list-stop-button',
component: PlayListStopButton,
group: 'playlist',
condition: showPlayButtons,
},
{
key: 'play-list-next-button',
component: PlayListNextButton,
group: 'playlist',
condition: showPlayButtons,
},
{
key: 'back-to-dashboard-button',
component: BackToDashboardButton,
group: 'panel',
condition: hasEditView || ((isViewingPanel || isEditingPanel) && !isEditingLibraryPanel),
},
{
key: 'discard-panel-button',
component: DiscardPanelButton,
group: 'panel',
condition: showPanelButtons && !isEditingLibraryPanel,
},
{
key: 'discard-library-panel-button',
component: DiscardLibraryPanelButton,
group: 'panel',
condition: showPanelButtons && isEditingLibraryPanel,
},
{
key: 'unlink-library-panel-button',
component: UnlinkLibraryPanelButton,
group: 'panel',
condition: showPanelButtons && isEditingLibraryPanel,
},
{
key: 'save-library-panel-button',
component: SaveLibraryPanelButton,
group: 'panel',
condition: showPanelButtons && isEditingLibraryPanel,
},
{
key: 'edit-schema-v2-button',
component: EditSchemaV2Button,
group: 'dashboard',
condition: isEditingAndShowingDashboard && hasUid,
},
{
key: 'dashboard-settings',
component: DashboardSettingsButton,
group: 'dashboard',
condition: isEditingAndShowingDashboard && dashboard.canEditDashboard(),
},
{
key: 'save-dashboard',
component: SaveDashboard,
group: 'save-edit',
condition: isEditingDashboard && !isEditingLibraryPanel && (canSave || canSaveInFolder),
},
{
key: 'make-dashboard-editable-button',
component: MakeDashboardEditableButton,
group: 'save-edit',
condition: !isEditing && dashboard.canEditDashboard() && !isViewingPanel && !isEditable,
},
{
key: 'edit-dashboard-switch',
component: EditDashboardSwitch,
group: 'save-edit',
condition: dashboard.canEditDashboard() && !isEditingLibraryPanel && !isViewingPanel && isEditable,
},
{
key: 'new-export-dashboard-button',
component: ExportDashboardButton,
group: 'export-share',
condition: showShareButton,
},
{
key: 'new-share-dashboard-button',
component: ShareDashboardButton,
group: 'export-share',
condition: showShareButton,
},
],
dashboard
)}
</ToolbarButtonRow>
);
};
const getStyles = () => ({
container: css({
flex: 1,
}),
});

@ -0,0 +1,19 @@
import { Stack } from '@grafana/ui';
import { DashboardScene } from '../DashboardScene';
import { LeftActions } from './LeftActions';
import { RightActions } from './RightActions';
interface Props {
dashboard: DashboardScene;
}
export function ToolbarActionsNew({ dashboard }: Props) {
return (
<Stack flex={1} minWidth={0}>
<LeftActions dashboard={dashboard} />
<RightActions dashboard={dashboard} />
</Stack>
);
}

@ -0,0 +1,22 @@
import { selectors } from '@grafana/e2e-selectors';
import { locationService } from '@grafana/runtime';
import { Button } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { ToolbarActionProps } from '../types';
export const BackToDashboardButton = ({ dashboard }: ToolbarActionProps) => (
<Button
onClick={() =>
locationService.partial(dashboard.state.editview ? { editview: null } : { viewPanel: null, editPanel: null })
}
tooltip=""
fill={dashboard.state.editview ? 'text' : undefined}
variant="secondary"
size="sm"
icon="arrow-left"
data-testid={selectors.components.NavToolbar.editDashboard.backToDashboardButton}
>
<Trans i18nKey="dashboard.toolbar.new.back-to-dashboard">Back to dashboard</Trans>
</Button>
);

@ -0,0 +1,14 @@
import { selectors } from '@grafana/e2e-selectors';
import { Icon, ToolbarButton } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { ToolbarActionProps } from '../types';
export const DashboardSettingsButton = ({ dashboard }: ToolbarActionProps) => (
<ToolbarButton
tooltip={t('dashboard.toolbar.new.dashboard-settings.tooltip', 'Dashboard settings')}
icon={<Icon name="cog" size="lg" type="default" />}
onClick={() => dashboard.onOpenSettings()}
data-testid={selectors.components.NavToolbar.editDashboard.settingsButton}
/>
);

@ -0,0 +1,18 @@
import { selectors } from '@grafana/e2e-selectors';
import { Button } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ToolbarActionProps } from '../types';
export const DiscardLibraryPanelButton = ({ dashboard }: ToolbarActionProps) => (
<Button
onClick={() => dashboard.state.editPanel?.onDiscard()}
tooltip={t('dashboard.toolbar.new.discard-library-panel-changes', 'Discard library panel changes')}
size="sm"
fill="outline"
variant="destructive"
data-testid={selectors.components.NavToolbar.editDashboard.discardChangesButton}
>
<Trans i18nKey="dashboard.toolbar.new.discard-library-panel-changes">Discard library panel changes</Trans>
</Button>
);

@ -0,0 +1,54 @@
import { useEffect, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Button } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { PanelEditor } from '../../../panel-edit/PanelEditor';
import { ToolbarActionProps } from '../types';
export const DiscardPanelButton = ({ dashboard }: ToolbarActionProps) => {
const isEditedPanelDirty = usePanelEditDirty(dashboard.state.editPanel);
return (
<Button
onClick={() => dashboard.state.editPanel?.onDiscard()}
tooltip={
dashboard.state.editPanel?.state.isNewPanel
? t('dashboard.toolbar.new.discard-panel-new', 'Discard panel')
: t('dashboard.toolbar.new.discard-panel', 'Discard panel changes')
}
size="sm"
disabled={!isEditedPanelDirty}
fill="outline"
variant="destructive"
data-testid={selectors.components.NavToolbar.editDashboard.discardChangesButton}
>
{dashboard.state.editPanel?.state.isNewPanel ? (
<Trans i18nKey="dashboard.toolbar.new.discard-panel-new">Discard panel</Trans>
) : (
<Trans i18nKey="dashboard.toolbar.new.discard-panel">Discard panel changes</Trans>
)}
</Button>
);
};
function usePanelEditDirty(panelEditor?: PanelEditor) {
const [isDirty, setIsDirty] = useState<Boolean | undefined>();
useEffect(() => {
if (panelEditor) {
const unsub = panelEditor.subscribeToState((state) => {
if (state.isDirty !== isDirty) {
setIsDirty(state.isDirty);
}
});
return () => unsub.unsubscribe();
}
return;
}, [panelEditor, isDirty]);
return isDirty;
}

@ -0,0 +1,28 @@
import { selectors } from '@grafana/e2e-selectors';
import { t } from 'app/core/internationalization';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { ToolbarActionProps } from '../types';
import { ToolbarSwitch } from './ToolbarSwitch';
export const EditDashboardSwitch = ({ dashboard }: ToolbarActionProps) => (
<ToolbarSwitch
checked={!!dashboard.state.isEditing}
icon="pen"
label={t('dashboard.toolbar.new.edit-toggle.enter.label', 'Enter edit mode')}
checkedLabel={t('dashboard.toolbar.new.edit-toggle.exit.label', 'Exit edit mode')}
disabled={playlistSrv.state.isPlaying}
data-testid={selectors.components.NavToolbar.editDashboard.editButton}
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
if (!dashboard.state.isEditing) {
dashboard.onEnterEditMode();
} else {
dashboard.exitEditMode({ skipConfirm: false });
}
}}
/>
);

@ -0,0 +1,12 @@
import { Icon, ToolbarButton } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { ToolbarActionProps } from '../types';
export const EditSchemaV2Button = ({ dashboard }: ToolbarActionProps) => (
<ToolbarButton
tooltip={t('dashboard.toolbar.new.edit-dashboard-v2-schema.tooltip', 'Edit dashboard v2 schema')}
icon={<Icon name="brackets-curly" size="lg" type="default" />}
onClick={() => dashboard.openV2SchemaEditor()}
/>
);

@ -0,0 +1,33 @@
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { locationService } from '@grafana/runtime';
import { t } from 'app/core/internationalization';
import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
import ExportMenu from '../../../sharing/ExportButton/ExportMenu';
import { DashboardInteractions } from '../../../utils/interactions';
import { ToolbarActionProps } from '../types';
import { ShareExportDashboardButton } from './ShareExportDashboardButton';
const newExportButtonSelector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton;
export const ExportDashboardButton = ({ dashboard }: ToolbarActionProps) => (
<ShareExportDashboardButton
menu={() => <ExportMenu dashboard={dashboard} />}
groupTestId={newExportButtonSelector.container}
buttonLabel={t('dashboard.toolbar.new.export.title', 'Export')}
buttonTooltip={t('dashboard.toolbar.new.export.tooltip', 'Export as JSON')}
buttonTestId={newExportButtonSelector.container}
onButtonClick={() => {
locationService.partial({ shareView: shareDashboardType.export });
DashboardInteractions.sharingCategoryClicked({
item: shareDashboardType.export,
shareResource: getTrackingSource(),
});
}}
arrowLabel={t('dashboard.toolbar.new.export.arrow', 'Export')}
arrowTestId={newExportButtonSelector.arrowMenu}
dashboard={dashboard}
/>
);

@ -0,0 +1,22 @@
import { selectors } from '@grafana/e2e-selectors';
import { Button } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { ToolbarActionProps } from '../types';
export const MakeDashboardEditableButton = ({ dashboard }: ToolbarActionProps) => (
<Button
disabled={playlistSrv.state.isPlaying}
onClick={() => {
dashboard.onEnterEditMode();
dashboard.setState({ editable: true, meta: { ...dashboard.state.meta, canEdit: true } });
}}
tooltip={t('dashboard.toolbar.new.enter-edit-mode.tooltip', 'This dashboard was marked as read only')}
variant="secondary"
size="sm"
data-testid={selectors.components.NavToolbar.editDashboard.editButton}
>
<Trans i18nKey="dashboard.toolbar.new.enter-edit-mode.label">Make editable</Trans>
</Button>
);

@ -0,0 +1,31 @@
import { Badge } from '@grafana/ui';
import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types';
import { ToolbarActionProps } from '../types';
export const ManagedDashboardBadge = ({ dashboard }: ToolbarActionProps) => {
if (!dashboard.state.meta.k8s?.annotations) {
return null;
}
let text = 'Provisioned';
const kind = dashboard.state.meta.k8s.annotations[AnnoKeyManagerKind];
const id = dashboard.state.meta.k8s.annotations[AnnoKeyManagerIdentity];
switch (kind) {
case ManagerKind.Terraform:
text = 'Terraform';
break;
case ManagerKind.Kubectl:
text = 'Kubectl';
break;
case ManagerKind.Plugin:
text = `Plugin: ${id}`;
break;
case ManagerKind.Repo:
text = 'Repository';
break;
}
return <Badge color="darkgrey" icon="exchange-alt" text={text} />;
};

@ -0,0 +1,6 @@
import { GoToSnapshotOriginButton } from '../../GoToSnapshotOriginButton';
import { ToolbarActionProps } from '../types';
export const OpenSnapshotOriginButton = ({ dashboard }: ToolbarActionProps) => (
<GoToSnapshotOriginButton originalURL={dashboard.getSnapshotUrl() ?? ''} />
);

@ -0,0 +1,16 @@
import { selectors } from '@grafana/e2e-selectors';
import { ToolbarButton } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { ToolbarActionProps } from '../types';
export const PlayListNextButton = ({}: ToolbarActionProps) => (
<ToolbarButton
data-testid={selectors.pages.Dashboard.DashNav.playlistControls.next}
tooltip={t('dashboard.toolbar.new.playlist-next', 'Go to next dashboard')}
icon="forward"
onClick={() => playlistSrv.next()}
narrow
/>
);

@ -0,0 +1,15 @@
import { selectors } from '@grafana/e2e-selectors';
import { ToolbarButton } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { ToolbarActionProps } from '../types';
export const PlayListPreviousButton = ({}: ToolbarActionProps) => (
<ToolbarButton
data-testid={selectors.pages.Dashboard.DashNav.playlistControls.prev}
tooltip={t('dashboard.toolbar.new.playlist-previous', 'Go to previous dashboard')}
icon="backward"
onClick={() => playlistSrv.prev()}
/>
);

@ -0,0 +1,15 @@
import { selectors } from '@grafana/e2e-selectors';
import { ToolbarButton } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { ToolbarActionProps } from '../types';
export const PlayListStopButton = ({}: ToolbarActionProps) => (
<ToolbarButton
onClick={() => playlistSrv.stop()}
data-testid={selectors.pages.Dashboard.DashNav.playlistControls.stop}
>
<Trans i18nKey="dashboard.toolbar.new.playlist-stop">Stop playlist</Trans>
</ToolbarButton>
);

@ -0,0 +1,28 @@
import { css } from '@emotion/css';
import { selectors } from '@grafana/e2e-selectors';
import { Badge, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { ToolbarActionProps } from '../types';
export const PublicDashboardBadge = ({}: ToolbarActionProps) => {
const styles = useStyles2(getStyles);
return (
<Badge
color="blue"
text={t('dashboard.toolbar.new.public-dashboard', 'Public')}
className={styles.badge}
data-testid={selectors.pages.Dashboard.DashNav.publicDashboardTag}
/>
);
};
const getStyles = () => ({
badge: css({
color: 'grey',
backgroundColor: 'transparent',
border: '1px solid',
}),
});

@ -0,0 +1,80 @@
import { selectors } from '@grafana/e2e-selectors';
import { Button, ButtonGroup, Dropdown, Menu } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv';
import { ToolbarActionProps } from '../types';
import { useIsManagedRepository } from '../utils';
export const SaveDashboard = ({ dashboard }: ToolbarActionProps) => {
const { meta, isDirty, uid } = dashboard.state;
const isNew = !Boolean(uid || dashboard.isManaged());
const isManagedRepository = useIsManagedRepository(dashboard);
// if we only can save
if (isNew) {
return (
<Button
onClick={() => dashboard.openSaveDrawer({})}
tooltip={t('dashboard.toolbar.new.save-dashboard.tooltip', 'Save changes')}
size="sm"
variant="primary"
data-testid={selectors.components.NavToolbar.editDashboard.saveButton}
>
<Trans i18nKey="dashboard.toolbar.new.save-dashboard.label">Save</Trans>
</Button>
);
}
// If we only can save as copy
if (contextSrv.hasEditPermissionInFolders && !meta.canSave && !meta.canMakeEditable && !isManagedRepository) {
return (
<Button
onClick={() => dashboard.openSaveDrawer({ saveAsCopy: true })}
tooltip={t('dashboard.toolbar.new.save-dashboard-copy.tooltip', 'Save as copy')}
size="sm"
variant={isDirty ? 'primary' : 'secondary'}
>
<Trans i18nKey="dashboard.toolbar.new.save-dashboard-copy.label">Save as copy</Trans>
</Button>
);
}
return (
<ButtonGroup>
<Button
onClick={() => dashboard.openSaveDrawer({})}
tooltip={t('dashboard.toolbar.new.save-dashboard.tooltip', 'Save changes')}
size="sm"
data-testid={selectors.components.NavToolbar.editDashboard.saveButton}
variant={isDirty ? 'primary' : 'secondary'}
>
<Trans i18nKey="dashboard.toolbar.new.save-dashboard.label">Save</Trans>
</Button>
<Dropdown
overlay={
<Menu>
<Menu.Item
label={t('dashboard.toolbar.new.save-dashboard-short', 'Save')}
icon="save"
onClick={() => dashboard.openSaveDrawer({})}
/>
<Menu.Item
label={t('dashboard.toolbar.new.save-dashboard-copy.label', 'Save as copy')}
icon="copy"
onClick={() => dashboard.openSaveDrawer({ saveAsCopy: true })}
/>
</Menu>
}
>
<Button
aria-label={t('dashboard.toolbar.new.more-save-options', 'More save options')}
icon="angle-down"
variant={isDirty ? 'primary' : 'secondary'}
size="sm"
/>
</Dropdown>
</ButtonGroup>
);
};

@ -0,0 +1,18 @@
import { selectors } from '@grafana/e2e-selectors';
import { Button } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ToolbarActionProps } from '../types';
export const SaveLibraryPanelButton = ({ dashboard }: ToolbarActionProps) => (
<Button
onClick={() => dashboard.state.editPanel?.onSaveLibraryPanel()}
tooltip={t('dashboard.toolbar.new.save-library-panel', 'Save library panel')}
size="sm"
fill="outline"
variant="primary"
data-testid={selectors.components.NavToolbar.editDashboard.saveLibraryPanelButton}
>
<Trans i18nKey="dashboard.toolbar.new.save-library-panel">Save library panel</Trans>
</Button>
);

@ -0,0 +1,40 @@
import { useAsyncFn } from 'react-use';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { t } from 'app/core/internationalization';
import ShareMenu from '../../../sharing/ShareButton/ShareMenu';
import { buildShareUrl } from '../../../sharing/ShareButton/utils';
import { DashboardInteractions } from '../../../utils/interactions';
import { ToolbarActionProps } from '../types';
import { ShareExportDashboardButton } from './ShareExportDashboardButton';
const newShareButtonSelector = e2eSelectors.pages.Dashboard.DashNav.newShareButton;
export const ShareDashboardButton = ({ dashboard }: ToolbarActionProps) => {
const [_, buildUrl] = useAsyncFn(async () => {
DashboardInteractions.toolbarShareClick();
return await buildShareUrl(dashboard);
}, [dashboard]);
return (
<ShareExportDashboardButton
menu={() => <ShareMenu dashboard={dashboard} />}
onMenuVisibilityChange={(isOpen) => {
if (isOpen) {
DashboardInteractions.toolbarShareDropdownClick();
}
}}
groupTestId={newShareButtonSelector.shareLink}
buttonLabel={t('dashboard.toolbar.new.share.title', 'Share')}
buttonTooltip={t('dashboard.toolbar.new.share.tooltip', 'Copy link')}
buttonTestId={newShareButtonSelector.container}
onButtonClick={buildUrl}
arrowLabel={t('dashboard.toolbar.new.share.arrow', 'Share')}
arrowTestId={newShareButtonSelector.arrowMenu}
dashboard={dashboard}
variant={!dashboard.state.isEditing ? 'primary' : 'secondary'}
/>
);
};

@ -0,0 +1,100 @@
import { css } from '@emotion/css';
import { ReactElement, useState } from 'react';
import { Button, ButtonGroup, Dropdown, useStyles2 } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { t } from 'app/core/internationalization';
import { ShowConfirmModalEvent } from 'app/types/events';
import { ToolbarActionProps } from '../types';
interface Props extends ToolbarActionProps {
menu: ReactElement | (() => ReactElement);
onMenuVisibilityChange?: (isOpen: boolean) => void;
groupTestId: string;
buttonLabel: string;
buttonTooltip: string;
buttonTestId: string;
onButtonClick?: () => void;
arrowLabel: string;
arrowTestId: string;
variant?: 'primary' | 'secondary';
}
export const ShareExportDashboardButton = ({
dashboard,
menu,
onMenuVisibilityChange,
groupTestId,
buttonLabel,
buttonTooltip,
buttonTestId,
onButtonClick,
arrowLabel,
arrowTestId,
variant = 'secondary',
}: Props) => {
const styles = useStyles2(getStyles);
const [isOpen, setIsOpen] = useState(false);
return (
<ButtonGroup
data-testid={groupTestId}
className={styles.container}
onPointerDown={(evt) => {
if (dashboard.state.isEditing && dashboard.state.isDirty) {
evt.preventDefault();
evt.stopPropagation();
appEvents.publish(
new ShowConfirmModalEvent({
title: t('dashboard.toolbar.new.share-export.modal.title', 'Save changes to dashboard?'),
text: t(
'dashboard.toolbar.new.share-export.modal.text',
'You have unsaved changes to this dashboard. You need to save them before you can share it.'
),
icon: 'exclamation-triangle',
noText: t('dashboard.toolbar.new.share-export.modal.noText', 'Discard'),
yesText: t('dashboard.toolbar.new.share-export.modal.yesText', 'Save'),
yesButtonVariant: 'primary',
onConfirm: () => dashboard.openSaveDrawer({}),
})
);
}
}}
>
<Button data-testid={buttonTestId} size="sm" tooltip={buttonTooltip} variant={variant} onClick={onButtonClick}>
{buttonLabel}
</Button>
<Dropdown
overlay={menu}
placement="bottom-end"
onVisibleChange={(isOpen) => {
if (dashboard.state.isEditing && dashboard.state.isDirty) {
return;
}
onMenuVisibilityChange?.(isOpen);
setIsOpen(isOpen);
}}
>
<Button
aria-label={arrowLabel}
data-testid={arrowTestId}
size="sm"
icon={isOpen ? 'angle-up' : 'angle-down'}
variant={variant}
/>
</Dropdown>
</ButtonGroup>
);
};
function getStyles() {
return {
container: css({
gap: 1,
}),
};
}

@ -0,0 +1,28 @@
import { selectors } from '@grafana/e2e-selectors';
import { Icon, ToolbarButton } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { DashboardInteractions } from '../../../utils/interactions';
import { ToolbarActionProps } from '../types';
export const StarButton = ({ dashboard }: ToolbarActionProps) => (
<ToolbarButton
tooltip={
dashboard.state.meta.isStarred
? t('dashboard.toolbar.new.unmark-favorite', 'Unmark as favorite')
: t('dashboard.toolbar.new.mark-favorite', 'Mark as favorite')
}
icon={
<Icon
name={dashboard.state.meta.isStarred ? 'favorite' : 'star'}
size="lg"
type={dashboard.state.meta.isStarred ? 'mono' : 'default'}
/>
}
data-testid={selectors.components.NavToolbar.markAsFavorite}
onClick={() => {
DashboardInteractions.toolbarFavoritesClick();
dashboard.onStarDashboard();
}}
/>
);

@ -0,0 +1,119 @@
import { css, cx } from '@emotion/css';
import { MouseEvent } from 'react';
import { GrafanaTheme2, IconName } from '@grafana/data';
import { Icon, Tooltip, useStyles2 } from '@grafana/ui';
interface Props {
icon: IconName;
label: string;
checked: boolean;
checkedIcon?: IconName;
checkedLabel?: string;
disabled?: boolean;
'data-testId'?: string;
onClick: (evt: MouseEvent<HTMLDivElement>) => void;
}
export const ToolbarSwitch = ({
icon,
label,
checked,
checkedIcon,
checkedLabel,
disabled,
onClick,
'data-testId': dataTestId,
}: Props) => {
const styles = useStyles2(getStyles);
const labelText = checked && checkedLabel ? checkedLabel : label;
const iconName = checked && checkedIcon ? checkedIcon : icon;
return (
<Tooltip content={labelText}>
<div
aria-label={labelText}
role="button"
className={cx({
[styles.container]: true,
[styles.containerChecked]: checked,
[styles.containerDisabled]: disabled,
})}
data-testid={dataTestId}
onClick={disabled ? undefined : onClick}
>
<div className={cx(styles.box, checked && styles.boxChecked)}>
<Icon name={iconName} size="xs" />
</div>
</div>
</Tooltip>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
border: `1px solid ${theme.components.input.borderColor}`,
padding: theme.spacing(0.25),
backgroundColor: theme.components.input.background,
borderRadius: theme.shape.radius.default,
width: theme.spacing(5.5),
height: theme.spacing(3),
cursor: 'pointer',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
transition: 'all 0.2s ease-in-out',
},
'&:hover': {
borderColor: theme.components.input.borderHover,
},
}),
containerChecked: css({
backgroundColor: theme.colors.primary.main,
borderColor: 'transparent',
'&:hover': {
backgroundColor: theme.colors.primary.shade,
borderColor: 'transparent',
},
}),
containerDisabled: css({
cursor: 'initial',
background: theme.colors.action.disabledBackground,
borderColor: theme.colors.border.weak,
}),
box: css({
background: theme.colors.background.primary,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: theme.spacing(2.5),
height: theme.spacing(2.5),
borderRadius: theme.shape.radius.default,
transform: 'translateX(0)',
position: 'relative',
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
transition: 'all 0.2s ease-in-out',
},
'&:after': css({
content: "''",
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: theme.shape.radius.default,
background: theme.colors.secondary.main,
border: `1px solid ${theme.colors.secondary.border}`,
}),
}),
boxChecked: css({
transform: `translateX(calc(100% - ${theme.spacing(0.25)}))`,
}),
});

@ -0,0 +1,18 @@
import { selectors } from '@grafana/e2e-selectors';
import { Button } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ToolbarActionProps } from '../types';
export const UnlinkLibraryPanelButton = ({ dashboard }: ToolbarActionProps) => (
<Button
onClick={() => dashboard.state.editPanel?.onUnlinkLibraryPanel()}
tooltip={t('dashboard.toolbar.new.unlink-library-panel', 'Unlink library panel')}
size="sm"
fill="outline"
variant="secondary"
data-testid={selectors.components.NavToolbar.editDashboard.unlinkLibraryPanelButton}
>
<Trans i18nKey="dashboard.toolbar.new.unlink-library-panel">Unlink library panel</Trans>
</Button>
);

@ -0,0 +1,14 @@
import { FC } from 'react';
import { DashboardScene } from '../DashboardScene';
export interface ToolbarAction {
key: string;
component: FC<ToolbarActionProps>;
group: string;
condition: boolean;
}
export interface ToolbarActionProps {
dashboard: DashboardScene;
}

@ -0,0 +1,62 @@
import { ReactNode } from 'react';
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { selectFolderRepository } from 'app/features/provisioning/utils/selectors';
import { useSelector } from 'app/types';
import { DynamicDashNavButtonModel } from '../../utils/registerDynamicDashNavAction';
import { DashboardScene } from '../DashboardScene';
import { ToolbarAction } from './types';
export function renderActionElements(toolbarActions: ToolbarAction[], dashboard: DashboardScene): ReactNode[] {
const actionElements: ReactNode[] = [];
let lastGroup = '';
for (const action of toolbarActions) {
if (!action.condition) {
continue;
}
if (lastGroup && lastGroup !== action.group) {
actionElements.push(<NavToolbarSeparator key={`${action.group}-separator`} />);
}
actionElements.push(<action.component key={action.key} dashboard={dashboard} />);
lastGroup = action.group;
}
return actionElements;
}
export function getDynamicActions(
registeredActions: DynamicDashNavButtonModel[],
group: string,
condition: boolean
): ToolbarAction[] {
const dashboard = getDashboardSrv().getCurrent()!;
return registeredActions.reduce<ToolbarAction[]>((acc, action) => {
const props = { dashboard };
if (!action.show(props)) {
return acc;
}
acc.push({
key: acc.length.toString(),
group,
condition,
component: () => <action.component {...props} />,
});
return acc;
}, []);
}
export function useIsManagedRepository(dashboard: DashboardScene): boolean {
const folderRepo = useSelector((state) => selectFolderRepository(state, dashboard.state.meta.folderUid));
return Boolean(dashboard.isManagedRepository() || folderRepo);
}

@ -10,7 +10,11 @@ import ExportMenu from './ExportMenu';
const newExportButtonSelector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton;
export default function ExportButton({ dashboard }: { dashboard: DashboardScene }) {
interface Props {
dashboard: DashboardScene;
}
export default function ExportButton({ dashboard }: Props) {
const [isOpen, setIsOpen] = useState(false);
const onMenuClick = useCallback((isOpen: boolean) => {

@ -1537,7 +1537,6 @@
"label": "Edit",
"tooltip": "Enter edit mode"
},
"edit-dashboard-v2-schema": "Edit dashboard v2 schema",
"enter-edit-mode": {
"label": "Make editable",
"tooltip": "This dashboard was marked as read only"
@ -1548,6 +1547,66 @@
},
"mark-favorite": "Mark as favorite",
"more-save-options": "More save options",
"new": {
"back-to-dashboard": "Back to dashboard",
"dashboard-settings": {
"tooltip": "Dashboard settings"
},
"discard-library-panel-changes": "Discard library panel changes",
"discard-panel": "Discard panel changes",
"discard-panel-new": "Discard panel",
"edit-dashboard-v2-schema": {
"tooltip": "Edit dashboard v2 schema"
},
"edit-toggle": {
"enter": {
"label": "Enter edit mode"
},
"exit": {
"label": "Exit edit mode"
}
},
"enter-edit-mode": {
"label": "Make editable",
"tooltip": "This dashboard was marked as read only"
},
"export": {
"arrow": "Export",
"title": "Export",
"tooltip": "Export as JSON"
},
"mark-favorite": "Mark as favorite",
"more-save-options": "More save options",
"playlist-next": "Go to next dashboard",
"playlist-previous": "Go to previous dashboard",
"playlist-stop": "Stop playlist",
"public-dashboard": "Public",
"save-dashboard": {
"label": "Save",
"tooltip": "Save changes"
},
"save-dashboard-copy": {
"label": "Save as copy",
"tooltip": "Save as copy"
},
"save-dashboard-short": "Save",
"save-library-panel": "Save library panel",
"share": {
"arrow": "Share",
"title": "Share",
"tooltip": "Copy link"
},
"share-export": {
"modal": {
"noText": "Discard",
"text": "You have unsaved changes to this dashboard. You need to save them before you can share it.",
"title": "Save changes to dashboard?",
"yesText": "Save"
}
},
"unlink-library-panel": "Unlink library panel",
"unmark-favorite": "Unmark as favorite"
},
"open-original": "Open original dashboard",
"playlist-next": "Go to next dashboard",
"playlist-previous": "Go to previous dashboard",
@ -1571,7 +1630,6 @@
"tooltip": "Share dashboard"
},
"share-button": "Share",
"show-hidden-elements": "Show hidden",
"switch-old-dashboard": "Switch to old dashboard page",
"unlink-library-panel": "Unlink library panel",
"unmark-favorite": "Unmark as favorite"

Loading…
Cancel
Save