Dynamic Dashboards: Implement new toolbar (#102195)
This commit is contained in:
@@ -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;
|
||||
|
||||
+8
-12
@@ -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>
|
||||
);
|
||||
}
|
||||
+22
@@ -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>
|
||||
);
|
||||
+14
@@ -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}
|
||||
/>
|
||||
);
|
||||
+18
@@ -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()}
|
||||
/>
|
||||
);
|
||||
+33
@@ -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}
|
||||
/>
|
||||
);
|
||||
+22
@@ -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>
|
||||
);
|
||||
+31
@@ -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} />;
|
||||
};
|
||||
+6
@@ -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
|
||||
/>
|
||||
);
|
||||
+15
@@ -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>
|
||||
);
|
||||
+28
@@ -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>
|
||||
);
|
||||
};
|
||||
+18
@@ -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>
|
||||
);
|
||||
+40
@@ -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'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+100
@@ -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)}))`,
|
||||
}),
|
||||
});
|
||||
+18
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user