mirror of https://github.com/grafana/grafana
Dynamic Dashboards: Implement new toolbar (#102195)
parent
47b94cf17b
commit
d3832c7f8b
@ -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); |
||||
} |
Loading…
Reference in new issue