Add clickable add button
This commit is contained in:
@@ -190,6 +190,9 @@ export const versionedPages = {
|
||||
outlineButton: {
|
||||
'12.4.0': 'data-testid Dashboard Sidebar outline button',
|
||||
},
|
||||
addButton: {
|
||||
'12.4.0': 'data-testid Dashboard Sidebar new button',
|
||||
}
|
||||
},
|
||||
DashNav: {
|
||||
nav: {
|
||||
|
||||
@@ -16,10 +16,11 @@ export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
active?: boolean;
|
||||
tooltip?: string;
|
||||
title: string;
|
||||
isAddButton?: boolean;
|
||||
}
|
||||
|
||||
export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
({ icon, active, onClick, title, tooltip, ...restProps }, ref) => {
|
||||
({ icon, active, onClick, title, tooltip, isAddButton, ...restProps }, ref) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const context = useContext(SidebarContext);
|
||||
|
||||
@@ -31,7 +32,8 @@ export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
styles.button,
|
||||
context.compact && styles.compact,
|
||||
active && styles.active,
|
||||
context.position === 'left' && styles.leftButton
|
||||
context.position === 'left' && styles.leftButton,
|
||||
isAddButton && 'primary'
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -44,7 +46,7 @@ export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
onClick={onClick}
|
||||
{...restProps}
|
||||
>
|
||||
<div className={styles.iconWrapper}>{renderIcon(icon, context.compact)}</div>
|
||||
<div>{renderIcon(icon, isAddButton)}</div>
|
||||
{!context.compact && <div className={cx(styles.title, active && styles.titleActive)}>{title}</div>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
@@ -54,13 +56,13 @@ export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
|
||||
SidebarButton.displayName = 'SidebarButton';
|
||||
|
||||
function renderIcon(icon: IconName | React.ReactNode, compact?: boolean) {
|
||||
function renderIcon(icon: IconName | React.ReactNode, isAddButton?: boolean) {
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isIconName(icon)) {
|
||||
return <Icon name={icon} size={compact ? `lg` : `lg`} />;
|
||||
return <Icon name={icon} size={isAddButton ? 'xl' : 'lg'} />;
|
||||
}
|
||||
|
||||
return icon;
|
||||
@@ -83,7 +85,14 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
color: theme.colors.text.secondary,
|
||||
background: 'transparent',
|
||||
border: `none`,
|
||||
|
||||
'&.primary': css({
|
||||
svg: {
|
||||
backgroundColor: theme.colors.primary.main,
|
||||
color: theme.colors.getContrastText(theme.colors.primary.main),
|
||||
borderRadius: theme.shape.radius.sm,
|
||||
padding: 2,
|
||||
},
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create(['background-color', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
@@ -145,7 +154,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
width: '100%',
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
iconWrapper: css({}),
|
||||
title: css({
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
color: theme.colors.text.secondary,
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface DashboardEditPaneState extends SceneObjectState {
|
||||
isDocked?: boolean;
|
||||
}
|
||||
|
||||
export type DashboardSidebarPaneName = 'element' | 'outline' | 'filters';
|
||||
export type DashboardSidebarPaneName = 'element' | 'outline' | 'filters' | 'add';
|
||||
|
||||
export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
||||
public constructor() {
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useSceneObjectState } from '@grafana/scenes';
|
||||
import { SceneObject, SceneObjectState, useSceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
import { Sidebar } from '@grafana/ui';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { onOpenSnapshotOriginalDashboard } from '../scene/GoToSnapshotOriginButton';
|
||||
import { ManagedDashboardNavBarBadge } from '../scene/ManagedDashboardNavBarBadge';
|
||||
import { RowItem } from '../scene/layout-rows/RowItem';
|
||||
import { TabItem } from '../scene/layout-tabs/TabItem';
|
||||
import { ToolbarActionProps } from '../scene/new-toolbar/types';
|
||||
import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction';
|
||||
import { getDefaultVizPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardEditPane } from './DashboardEditPane';
|
||||
import { ShareExportDashboardButton } from './DashboardExportButton';
|
||||
import { DashboardOutline } from './DashboardOutline';
|
||||
import { DashboardSidePaneNew } from './DashboardSidePaneNew';
|
||||
import { ElementEditPane } from './ElementEditPane';
|
||||
|
||||
export interface Props {
|
||||
editPane: DashboardEditPane;
|
||||
dashboard: DashboardScene;
|
||||
isDocked?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Making the EditPane rendering completely standalone (not using editPane.Component) in order to pass custom react props
|
||||
*/
|
||||
export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Props) {
|
||||
export function DashboardEditPaneRenderer({ editPane, dashboard }: Props) {
|
||||
const { selection, openPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
|
||||
const { isEditing, meta, uid } = dashboard.useState();
|
||||
const hasUid = Boolean(uid);
|
||||
const selectedObject = selection?.getFirstObject();
|
||||
const isNewElement = selection?.isNewElement() ?? false;
|
||||
const [selectedLayoutElement, setSelectedLayoutElement] = useState(selectedObject);
|
||||
|
||||
const editableElement = useMemo(() => {
|
||||
if (selection) {
|
||||
@@ -42,6 +46,26 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro
|
||||
return undefined;
|
||||
}, [selection]);
|
||||
|
||||
// saves the row or tab selected when opening the 'add' pane, so new panels can later be added to it
|
||||
const onSetLayoutElement = (obj: SceneObject<SceneObjectState> | undefined) => {
|
||||
if (obj instanceof RowItem || obj instanceof TabItem) {
|
||||
setSelectedLayoutElement(obj);
|
||||
} else if (!(obj instanceof VizPanel)) {
|
||||
setSelectedLayoutElement(dashboard);
|
||||
}
|
||||
};
|
||||
|
||||
const onAddNewPanel = () => {
|
||||
if (selectedLayoutElement) {
|
||||
const panel = getDefaultVizPanel();
|
||||
if (selectedLayoutElement instanceof DashboardScene) {
|
||||
dashboard.addPanel(panel);
|
||||
} else if (selectedLayoutElement instanceof RowItem || selectedLayoutElement instanceof TabItem) {
|
||||
selectedLayoutElement.getLayout().addPanel(panel);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{editableElement && (
|
||||
@@ -54,6 +78,11 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro
|
||||
/>
|
||||
</Sidebar.OpenPane>
|
||||
)}
|
||||
{openPane === 'add' && (
|
||||
<Sidebar.OpenPane>
|
||||
<DashboardSidePaneNew onAddPanel={onAddNewPanel} />
|
||||
</Sidebar.OpenPane>
|
||||
)}
|
||||
{openPane === 'outline' && (
|
||||
<Sidebar.OpenPane>
|
||||
<DashboardOutline editPane={editPane} isEditing={isEditing} />
|
||||
@@ -68,6 +97,18 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro
|
||||
<RedoButton dashboard={dashboard} />
|
||||
</>
|
||||
)}
|
||||
<Sidebar.Button
|
||||
icon="plus"
|
||||
isAddButton
|
||||
onClick={() => {
|
||||
onSetLayoutElement(selectedObject);
|
||||
editPane.openPane('add');
|
||||
}}
|
||||
title={t('dashboard.sidebar.add.title', 'Add')}
|
||||
tooltip={t('dashboard.sidebar.add.tooltip', 'Add new element')}
|
||||
data-testid={selectors.pages.Dashboard.Sidebar.addButton}
|
||||
active={selectedObject === null || openPane === 'add'}
|
||||
/>
|
||||
<Sidebar.Button
|
||||
icon="cog"
|
||||
onClick={() => editPane.selectObject(dashboard, dashboard.state.key!)}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Sidebar, Text, useStyles2 } from '@grafana/ui';
|
||||
import addPanelImg from 'img/dashboards/add-panel.png';
|
||||
|
||||
|
||||
export function DashboardSidePaneNew({ onAddPanel }: {onAddPanel: () => void}) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.sidePanel}>
|
||||
<Sidebar.PaneHeader title={t('dashboard.add.pane-header', 'Add')} />
|
||||
<div className={styles.sidePanelContainer}>
|
||||
<Text weight="medium">{t('dashboard.add.new-panel.title', 'Panel')}</Text>
|
||||
<Text variant="bodySmall">
|
||||
{t('dashboard.add.new-panel.description', 'Drag or click to add a panel')}
|
||||
</Text>
|
||||
|
||||
<div className={styles.imageContainer}>
|
||||
<button className={styles.noStyles} onClick={onAddPanel} aria-label={t('dashboard.add.new-panel.title', 'Panel')}>
|
||||
<img alt="Add panel click area" src={addPanelImg} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
sidePanel: css({
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
sidePanelContainer: css({
|
||||
padding: theme.spacing(2),
|
||||
span: {
|
||||
display: 'block',
|
||||
},
|
||||
}),
|
||||
imageContainer: css({
|
||||
opacity: 0.8,
|
||||
padding: theme.spacing(2, 0),
|
||||
borderRadius: theme.shape.radius.sm,
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
}
|
||||
}),
|
||||
noStyles: css({
|
||||
all: 'unset',
|
||||
}),
|
||||
};
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -5216,6 +5216,13 @@
|
||||
"title-matched_one": "Matched {{count}}/{{totalCount}} options",
|
||||
"title-matched_other": "Matched {{count}}/{{totalCount}} options"
|
||||
},
|
||||
"add": {
|
||||
"pane-header": "Add",
|
||||
"new-panel": {
|
||||
"title": "Panel",
|
||||
"description": "Drag or click to add a panel"
|
||||
}
|
||||
},
|
||||
"outline": {
|
||||
"pane-header": "Content outline",
|
||||
"repeated-item": "Repeat",
|
||||
@@ -5433,6 +5440,10 @@
|
||||
"title": "Options",
|
||||
"tooltip": "Dashboard options"
|
||||
},
|
||||
"add": {
|
||||
"title": "Add",
|
||||
"tooltip": "Add a new element"
|
||||
},
|
||||
"edit-schema": {
|
||||
"title": "Code",
|
||||
"tooltip": "Edit as code"
|
||||
|
||||
Reference in New Issue
Block a user