diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index d8671716331..6b6c0b54f50 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -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(({ dashboard }) => { - const id = useId(); + const hasNewToolbar = config.featureToggles.dashboardNewLayouts && config.featureToggles.newDashboardSharingComponent; - const actions = ; + const actions = hasNewToolbar ? ( + + ) : ( + + ); return ; }); @@ -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: () => ( - - { - evt.stopPropagation(); - dashboard.onToggleHiddenElements(); - }} - data-testid={selectors.components.PageToolbar.itemButton('toggle_hidden_elements')} - /> - - Show hidden - - - ), - }); - } else { - toolbarActions.push({ - group: 'add-panel', - condition: isEditingAndShowingDashboard, - render: () => ( - { - setIsAddPanelMenuOpen(isOpen); - DashboardInteractions.toolbarAddClick(); - }} - overlay={() => ( - - { - const vizPanel = dashboard.onCreateNewPanel(); - DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' }); - dashboard.setState({ editPanel: buildPanelEditScene(vizPanel, true) }); - }} - /> - { - dashboard.onShowAddLibraryPanelDrawer(); - DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' }); - }} - disabled={dashboard.isManagedRepository()} - /> - { - dashboard.onCreateNewRow(); - DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' }); - }} - /> - { - dashboard.pastePanel(); - DashboardInteractions.toolbarAddButtonClicked({ item: 'paste_panel' }); - }} - /> - - )} - placement="bottom" - offset={[0, 6]} + toolbarActions.push({ + group: 'add-panel', + condition: isEditingAndShowingDashboard, + render: () => ( + { + setIsAddPanelMenuOpen(isOpen); + DashboardInteractions.toolbarAddClick(); + }} + overlay={() => ( + + { + const vizPanel = dashboard.onCreateNewPanel(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' }); + dashboard.setState({ editPanel: buildPanelEditScene(vizPanel, true) }); + }} + /> + { + dashboard.onShowAddLibraryPanelDrawer(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' }); + }} + disabled={dashboard.isManagedRepository()} + /> + { + dashboard.onCreateNewRow(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' }); + }} + /> + { + dashboard.pastePanel(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'paste_panel' }); + }} + /> + + )} + placement="bottom" + offset={[0, 6]} + > + - - ), - }); - } + Add + + + + ), + }); toolbarActions.push({ group: 'playlist-actions', @@ -419,27 +398,25 @@ export function ToolbarActions({ dashboard }: Props) { render: () => , }); - if (!dashboardNewLayouts) { - toolbarActions.push({ - group: 'settings', - condition: isEditing && dashboard.canEditDashboard() && isShowingDashboard, - render: () => ( - - ), - }); - } + toolbarActions.push({ + group: 'settings', + condition: isEditing && dashboard.canEditDashboard() && isShowingDashboard, + render: () => ( + + ), + }); 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 ( - } - key="schema-v2-button" - onClick={() => { - dashboard.openV2SchemaEditor(); - }} - /> - ); - }, - }); - const rightActionsElements: ReactNode[] = renderActionElements(toolbarActions); const leftActionsElements: ReactNode[] = renderActionElements(leftActions); const hasActionsToLeftAndRight = leftActionsElements.length > 0; diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index fe17f5ca802..3cb79c576d4 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -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)), + }), + }) + ), }), }); } diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx new file mode 100644 index 00000000000..2318694ecd9 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx @@ -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 ( + + {elements} + + ); +}; + +const getStyles = () => ({ + container: css({ + flex: 1, + }), +}); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx new file mode 100644 index 00000000000..b809a9fb90a --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx @@ -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 ( + + {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 + )} + + ); +}; + +const getStyles = () => ({ + container: css({ + flex: 1, + }), +}); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/ToolbarActionsNew.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/ToolbarActionsNew.tsx new file mode 100644 index 00000000000..f7d5a9b7784 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/ToolbarActionsNew.tsx @@ -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 ( + + + + + ); +} diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/BackToDashboardButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/BackToDashboardButton.tsx new file mode 100644 index 00000000000..1bc9ba5920f --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/BackToDashboardButton.tsx @@ -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) => ( + +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/DashboardSettingsButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/DashboardSettingsButton.tsx new file mode 100644 index 00000000000..917e0a02c0a --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/DashboardSettingsButton.tsx @@ -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) => ( + } + onClick={() => dashboard.onOpenSettings()} + data-testid={selectors.components.NavToolbar.editDashboard.settingsButton} + /> +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/DiscardLibraryPanelButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/DiscardLibraryPanelButton.tsx new file mode 100644 index 00000000000..99d1397d11e --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/DiscardLibraryPanelButton.tsx @@ -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) => ( + +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/DiscardPanelButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/DiscardPanelButton.tsx new file mode 100644 index 00000000000..a1deec24a08 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/DiscardPanelButton.tsx @@ -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 ( + + ); +}; + +function usePanelEditDirty(panelEditor?: PanelEditor) { + const [isDirty, setIsDirty] = useState(); + + useEffect(() => { + if (panelEditor) { + const unsub = panelEditor.subscribeToState((state) => { + if (state.isDirty !== isDirty) { + setIsDirty(state.isDirty); + } + }); + + return () => unsub.unsubscribe(); + } + + return; + }, [panelEditor, isDirty]); + + return isDirty; +} diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditDashboardSwitch.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditDashboardSwitch.tsx new file mode 100644 index 00000000000..b5ff59c9566 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditDashboardSwitch.tsx @@ -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) => ( + { + evt.preventDefault(); + evt.stopPropagation(); + + if (!dashboard.state.isEditing) { + dashboard.onEnterEditMode(); + } else { + dashboard.exitEditMode({ skipConfirm: false }); + } + }} + /> +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditSchemaV2Button.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditSchemaV2Button.tsx new file mode 100644 index 00000000000..fc18a235bdc --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditSchemaV2Button.tsx @@ -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) => ( + } + onClick={() => dashboard.openV2SchemaEditor()} + /> +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx new file mode 100644 index 00000000000..4d361dfcdfb --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx @@ -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) => ( + } + 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} + /> +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.tsx new file mode 100644 index 00000000000..d544a6b9805 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.tsx @@ -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) => ( + +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ManagedDashboardBadge.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ManagedDashboardBadge.tsx new file mode 100644 index 00000000000..a795fa60a03 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ManagedDashboardBadge.tsx @@ -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 ; +}; diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx new file mode 100644 index 00000000000..c59d06a74ae --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx @@ -0,0 +1,6 @@ +import { GoToSnapshotOriginButton } from '../../GoToSnapshotOriginButton'; +import { ToolbarActionProps } from '../types'; + +export const OpenSnapshotOriginButton = ({ dashboard }: ToolbarActionProps) => ( + +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListNextButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListNextButton.tsx new file mode 100644 index 00000000000..de3c7583713 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListNextButton.tsx @@ -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) => ( + playlistSrv.next()} + narrow + /> +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListPreviousButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListPreviousButton.tsx new file mode 100644 index 00000000000..782253dc482 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListPreviousButton.tsx @@ -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) => ( + playlistSrv.prev()} + /> +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListStopButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListStopButton.tsx new file mode 100644 index 00000000000..0b8a0f10bac --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/PlayListStopButton.tsx @@ -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) => ( + playlistSrv.stop()} + data-testid={selectors.pages.Dashboard.DashNav.playlistControls.stop} + > + Stop playlist + +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/PublicDashboardBadge.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/PublicDashboardBadge.tsx new file mode 100644 index 00000000000..0194fb76d8f --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/PublicDashboardBadge.tsx @@ -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 ( + + ); +}; + +const getStyles = () => ({ + badge: css({ + color: 'grey', + backgroundColor: 'transparent', + border: '1px solid', + }), +}); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx new file mode 100644 index 00000000000..3600add803f --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx @@ -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 ( + + ); + } + + // If we only can save as copy + if (contextSrv.hasEditPermissionInFolders && !meta.canSave && !meta.canMakeEditable && !isManagedRepository) { + return ( + + ); + } + + return ( + + + + dashboard.openSaveDrawer({})} + /> + dashboard.openSaveDrawer({ saveAsCopy: true })} + /> + + } + > + +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx new file mode 100644 index 00000000000..8cacae1a4aa --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx @@ -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 ( + } + 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'} + /> + ); +}; diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareExportDashboardButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareExportDashboardButton.tsx new file mode 100644 index 00000000000..bebed306fbe --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareExportDashboardButton.tsx @@ -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 ( + { + 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({}), + }) + ); + } + }} + > + + { + if (dashboard.state.isEditing && dashboard.state.isDirty) { + return; + } + + onMenuVisibilityChange?.(isOpen); + + setIsOpen(isOpen); + }} + > + +); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/types.ts b/public/app/features/dashboard-scene/scene/new-toolbar/types.ts new file mode 100644 index 00000000000..6fe9a200b92 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/types.ts @@ -0,0 +1,14 @@ +import { FC } from 'react'; + +import { DashboardScene } from '../DashboardScene'; + +export interface ToolbarAction { + key: string; + component: FC; + group: string; + condition: boolean; +} + +export interface ToolbarActionProps { + dashboard: DashboardScene; +} diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/utils.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/utils.tsx new file mode 100644 index 00000000000..0b44e415a75 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/new-toolbar/utils.tsx @@ -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(); + } + + actionElements.push(); + lastGroup = action.group; + } + + return actionElements; +} + +export function getDynamicActions( + registeredActions: DynamicDashNavButtonModel[], + group: string, + condition: boolean +): ToolbarAction[] { + const dashboard = getDashboardSrv().getCurrent()!; + + return registeredActions.reduce((acc, action) => { + const props = { dashboard }; + + if (!action.show(props)) { + return acc; + } + + acc.push({ + key: acc.length.toString(), + group, + condition, + component: () => , + }); + + return acc; + }, []); +} + +export function useIsManagedRepository(dashboard: DashboardScene): boolean { + const folderRepo = useSelector((state) => selectFolderRepository(state, dashboard.state.meta.folderUid)); + + return Boolean(dashboard.isManagedRepository() || folderRepo); +} diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.tsx index 5fd21ab76c6..9c4aea1cda6 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.tsx @@ -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) => { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 7527f9d0682..0f8c5c3fac9 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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"