mirror of https://github.com/grafana/grafana.git
Dashboard: Edit pane UI updates (#102663)
* First pass * Update * Outline tweaks * Update * Updates * Minor fixes * fix * Fixes * Fixes * Collapse add element pane when selecting
This commit is contained in:
parent
91baf78071
commit
648e68387e
|
|
@ -170,6 +170,7 @@ export const availableIconsIndex = {
|
|||
kubernetes: true,
|
||||
'layer-group': true,
|
||||
'layers-alt': true,
|
||||
layers: true,
|
||||
'legend-hide': true,
|
||||
'legend-show': true,
|
||||
'library-panel': true,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export class ConditionalRenderingData extends ConditionalRenderingBase<Condition
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
panelDataProviders.forEach((d) => {
|
||||
this._subs.add(
|
||||
d.subscribeToState(() => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Box, Card, Icon, IconName, useStyles2 } from '@grafana/ui';
|
||||
import { Box, Card, Icon, IconButton, IconName, useStyles2 } from '@grafana/ui';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import store from 'app/core/store';
|
||||
|
|
@ -84,22 +85,35 @@ export function DashboardAddPane({ editPane }: Props) {
|
|||
];
|
||||
|
||||
return (
|
||||
<Box display="flex" direction="column" gap={1} padding={2}>
|
||||
{cards.map(({ icon, heading, title, testId, onClick, hide }) =>
|
||||
hide ? null : (
|
||||
<Card onClick={onClick} data-testid={testId} title={title} key={title}>
|
||||
<Card.Heading>{heading}</Card.Heading>
|
||||
<Card.Figure className={styles.figure}>
|
||||
<Icon name={icon} size="xl" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
<>
|
||||
<div className={styles.header}>
|
||||
<IconButton name="arrow-left" size="lg" onClick={() => editPane.toggleAddPane()} aria-label="Close add pane" />
|
||||
{t('dashboard.edit-pane.add.title', 'Add element')}
|
||||
</div>
|
||||
<Box display="flex" direction="column" gap={1} padding={2}>
|
||||
{cards.map(({ icon, heading, title, testId, onClick, hide }) =>
|
||||
hide ? null : (
|
||||
<Card onClick={onClick} data-testid={testId} title={title} key={title}>
|
||||
<Card.Heading>{heading}</Card.Heading>
|
||||
<Card.Figure className={styles.figure}>
|
||||
<Icon name={icon} size="xl" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = () => ({
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
header: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(1, 2),
|
||||
gap: theme.spacing(1),
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
figure: css({
|
||||
pointerEvents: 'none',
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import { css, cx } from '@emotion/css';
|
||||
import { Resizable } from 're-resizable';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneObjectState, SceneObjectBase, SceneObject, sceneGraph, useSceneObjectState } from '@grafana/scenes';
|
||||
import {
|
||||
ElementSelectionContextItem,
|
||||
ElementSelectionContextState,
|
||||
Tab,
|
||||
TabsBar,
|
||||
ScrollContainer,
|
||||
ToolbarButton,
|
||||
useSplitter,
|
||||
useStyles2,
|
||||
Text,
|
||||
Icon,
|
||||
} from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
|
|
@ -27,7 +30,7 @@ import { useEditableElement } from './useEditableElement';
|
|||
export interface DashboardEditPaneState extends SceneObjectState {
|
||||
selection?: ElementSelection;
|
||||
selectionContext: ElementSelectionContextState;
|
||||
tab?: EditPaneTab;
|
||||
showAddPane?: boolean;
|
||||
}
|
||||
|
||||
export type EditPaneTab = 'add' | 'configure' | 'outline';
|
||||
|
|
@ -63,9 +66,7 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
|||
|
||||
this._subs.add(
|
||||
dashboard.subscribeToEvent(ObjectsReorderedOnCanvasEvent, ({ payload }) => {
|
||||
if (this.state.tab === 'outline') {
|
||||
this.forceRender();
|
||||
}
|
||||
this.forceRender();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -103,6 +104,10 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
|||
return this.state.selection?.getSelection();
|
||||
}
|
||||
|
||||
public toggleAddPane() {
|
||||
this.setState({ showAddPane: !this.state.showAddPane });
|
||||
}
|
||||
|
||||
public selectObject(obj: SceneObject, id: string, multi?: boolean) {
|
||||
const prevItem = this.state.selection?.getFirstObject();
|
||||
if (prevItem === obj && !multi) {
|
||||
|
|
@ -125,6 +130,7 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
|||
...this.state.selectionContext,
|
||||
selected,
|
||||
},
|
||||
showAddPane: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -163,15 +169,11 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
|||
});
|
||||
}
|
||||
|
||||
public onChangeTab = (tab: EditPaneTab) => {
|
||||
this.setState({ tab });
|
||||
};
|
||||
|
||||
private newObjectAddedToCanvas(obj: SceneObject) {
|
||||
this.selectObject(obj, obj.state.key!, false);
|
||||
|
||||
if (this.state.tab !== 'configure') {
|
||||
this.onChangeTab('configure');
|
||||
if (this.state.showAddPane) {
|
||||
this.setState({ showAddPane: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -202,11 +204,27 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
|
|||
}
|
||||
}, [editPane, isCollapsed]);
|
||||
|
||||
const { selection, tab = 'configure' } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
|
||||
const { selection, showAddPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
|
||||
const styles = useStyles2(getStyles);
|
||||
const paneRef = useRef<HTMLDivElement>(null);
|
||||
const editableElement = useEditableElement(selection, editPane);
|
||||
const selectedObject = selection?.getFirstObject();
|
||||
const [outlineCollapsed, setOutlineCollapsed] = useLocalStorage(
|
||||
'grafana.dashboard.edit-pane.outline.collapsed',
|
||||
false
|
||||
);
|
||||
const [outlinePaneSize = 0.4, setOutlinePaneSize] = useLocalStorage('grafana.dashboard.edit-pane.outline.size', 0.4);
|
||||
|
||||
// splitter for template and payload editor
|
||||
const splitter = useSplitter({
|
||||
direction: 'column',
|
||||
handleSize: 'sm',
|
||||
// if Grafana Alertmanager, split 50/50, otherwise 100/0 because there is no payload editor
|
||||
initialSize: 1 - outlinePaneSize,
|
||||
dragPosition: 'middle',
|
||||
onSizeChanged: (size) => {
|
||||
setOutlinePaneSize(1 - size);
|
||||
},
|
||||
});
|
||||
|
||||
if (!editableElement) {
|
||||
return null;
|
||||
|
|
@ -221,43 +239,67 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
|
|||
icon="arrow-to-right"
|
||||
onClick={onToggleCollapse}
|
||||
variant="canvas"
|
||||
narrow={true}
|
||||
className={styles.rotate180}
|
||||
aria-label={t('dashboard.edit-pane.open', 'Open options pane')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{openOverlay && (
|
||||
<Resizable className={cx(styles.fixed, styles.container)} defaultSize={{ height: '100%', width: '20vw' }}>
|
||||
<ElementEditPane element={editableElement} key={selectedObject?.state.key} />
|
||||
<Resizable className={styles.overlayWrapper} defaultSize={{ height: '100%', width: '300px' }}>
|
||||
<ElementEditPane element={editableElement} key={selectedObject?.state.key} editPane={editPane} />
|
||||
</Resizable>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (outlineCollapsed) {
|
||||
splitter.primaryProps.style.flexGrow = 1;
|
||||
splitter.primaryProps.style.minHeight = 'unset';
|
||||
splitter.secondaryProps.style.flexGrow = 0;
|
||||
splitter.secondaryProps.style.minHeight = 'min-content';
|
||||
} else {
|
||||
splitter.primaryProps.style.minHeight = 'unset';
|
||||
splitter.secondaryProps.style.minHeight = 'unset';
|
||||
}
|
||||
|
||||
if (showAddPane) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<DashboardAddPane editPane={editPane} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} ref={paneRef}>
|
||||
<TabsBar className={styles.tabsbar}>
|
||||
<Tab
|
||||
active={tab === 'add'}
|
||||
label={t('dashboard.editpane.add', 'Add')}
|
||||
onChangeTab={() => editPane.onChangeTab('add')}
|
||||
<div className={styles.wrapper}>
|
||||
<div {...splitter.containerProps}>
|
||||
<div {...splitter.primaryProps} className={cx(splitter.primaryProps.className, styles.paneContent)}>
|
||||
<ElementEditPane element={editableElement} key={selectedObject?.state.key} editPane={editPane} />
|
||||
</div>
|
||||
<div
|
||||
{...splitter.splitterProps}
|
||||
className={cx(splitter.splitterProps.className, styles.splitter)}
|
||||
data-edit-pane-splitter={true}
|
||||
/>
|
||||
<Tab
|
||||
active={tab === 'configure'}
|
||||
label={t('dashboard.editpane.configure', 'Configure')}
|
||||
onChangeTab={() => editPane.onChangeTab('configure')}
|
||||
/>
|
||||
<Tab
|
||||
active={tab === 'outline'}
|
||||
label={t('dashboard.editpane.outline', 'Outline')}
|
||||
onChangeTab={() => editPane.onChangeTab('outline')}
|
||||
/>
|
||||
</TabsBar>
|
||||
<div className={styles.tabContent}>
|
||||
{tab === 'add' && <DashboardAddPane editPane={editPane} />}
|
||||
{tab === 'configure' && <ElementEditPane element={editableElement} key={selectedObject?.state.key} />}
|
||||
{tab === 'outline' && <DashboardOutline editPane={editPane} />}
|
||||
<div {...splitter.secondaryProps} className={cx(splitter.primaryProps.className, styles.paneContent)}>
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => setOutlineCollapsed(!outlineCollapsed)}
|
||||
className={styles.outlineCollapseButton}
|
||||
>
|
||||
<Text weight="medium">Outline</Text>
|
||||
<Icon name="angle-up" />
|
||||
</div>
|
||||
{!outlineCollapsed && (
|
||||
<div className={styles.outlineContainer}>
|
||||
<ScrollContainer showScrollIndicators={true}>
|
||||
<DashboardOutline editPane={editPane} />
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -269,13 +311,27 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1 1 0',
|
||||
marginTop: theme.spacing(2),
|
||||
borderLeft: `1px solid ${theme.colors.border.weak}`,
|
||||
borderTop: `1px solid ${theme.colors.border.weak}`,
|
||||
background: theme.colors.background.primary,
|
||||
}),
|
||||
tabContent: css({
|
||||
overlayWrapper: css({
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: theme.spacing(2),
|
||||
position: 'absolute !important' as 'absolute',
|
||||
background: theme.colors.background.primary,
|
||||
borderLeft: `1px solid ${theme.colors.border.weak}`,
|
||||
borderTop: `1px solid ${theme.colors.border.weak}`,
|
||||
boxShadow: theme.shadows.z3,
|
||||
zIndex: theme.zIndex.navbarFixed,
|
||||
flexGrow: 1,
|
||||
}),
|
||||
paneContent: css({
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flex: '1 1 0',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
overflow: 'auto',
|
||||
}),
|
||||
rotate180: css({
|
||||
rotate: '180deg',
|
||||
|
|
@ -287,20 +343,30 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
expandOptionsWrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(2, 1),
|
||||
padding: theme.spacing(2, 1, 2, 0),
|
||||
}),
|
||||
// @ts-expect-error csstype doesn't allow !important. see https://github.com/frenic/csstype/issues/114
|
||||
fixed: css({
|
||||
position: 'absolute !important',
|
||||
splitter: css({
|
||||
'&:after': {
|
||||
display: 'none',
|
||||
},
|
||||
}),
|
||||
container: css({
|
||||
right: 0,
|
||||
background: theme.colors.background.primary,
|
||||
borderLeft: `1px solid ${theme.colors.border.weak}`,
|
||||
boxShadow: theme.shadows.z3,
|
||||
zIndex: theme.zIndex.navbarFixed,
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'scroll',
|
||||
outlineCollapseButton: css({
|
||||
display: 'flex',
|
||||
padding: theme.spacing(0.5, 2),
|
||||
gap: theme.spacing(1),
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
background: theme.colors.background.secondary,
|
||||
|
||||
'&:hover': {
|
||||
background: theme.colors.action.hover,
|
||||
},
|
||||
}),
|
||||
outlineContainer: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
|||
collapsed: isCollapsed,
|
||||
|
||||
paneOptions: {
|
||||
collapseBelowPixels: 250,
|
||||
collapseBelowPixels: 150,
|
||||
snapOpenToPixels: 400,
|
||||
},
|
||||
});
|
||||
|
|
@ -97,7 +97,11 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
|||
</div>
|
||||
{isEditing && (
|
||||
<>
|
||||
<div {...splitterProps} data-edit-pane-splitter={true} />
|
||||
<div
|
||||
{...splitterProps}
|
||||
className={cx(splitterProps.className, styles.splitter)}
|
||||
data-edit-pane-splitter={true}
|
||||
/>
|
||||
<div {...secondaryProps} className={cx(secondaryProps.className, styles.editPane)}>
|
||||
<DashboardEditPaneRenderer
|
||||
editPane={editPane}
|
||||
|
|
@ -156,11 +160,18 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number) {
|
|||
scrollbarWidth: 'thin',
|
||||
// The fixed controls headers is otherwise rendered over the selection outlinem, Maybe there is an other solution
|
||||
paddingTop: '2px',
|
||||
// Because the edit pane splitter handle area adds padding we can reduce it here
|
||||
paddingRight: theme.spacing(1),
|
||||
}),
|
||||
editPane: css({
|
||||
flexDirection: 'column',
|
||||
borderLeft: `1px solid ${theme.colors.border.weak}`,
|
||||
background: theme.colors.background.primary,
|
||||
// borderLeft: `1px solid ${theme.colors.border.weak}`,
|
||||
// background: theme.colors.background.primary,
|
||||
}),
|
||||
splitter: css({
|
||||
'&:after': {
|
||||
display: 'none',
|
||||
},
|
||||
}),
|
||||
controlsWrapperSticky: css({
|
||||
[theme.breakpoints.up('md')]: {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export class DashboardEditableElement implements EditableDashboardElement {
|
|||
return {
|
||||
typeName: t('dashboard.edit-pane.elements.dashboard', 'Dashboard'),
|
||||
icon: 'apps',
|
||||
instanceName: this.dashboard.state.title,
|
||||
instanceName: t('dashboard.edit-pane.elements.dashboard', 'Dashboard'),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +55,7 @@ export class DashboardEditableElement implements EditableDashboardElement {
|
|||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => this.dashboard.onOpenSettings()}
|
||||
tooltip={t('dashboard.toolbar.dashboard-settings.tooltip', 'Dashboard settings')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { useMemo, useState } from 'react';
|
|||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneObject, VizPanel } from '@grafana/scenes';
|
||||
import { Box, Icon, IconButton, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { Box, Icon, Text, useElementSelection, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { isInCloneChain } from '../utils/clone';
|
||||
|
|
@ -21,7 +21,7 @@ export function DashboardOutline({ editPane }: Props) {
|
|||
const dashboard = getDashboardSceneFor(editPane);
|
||||
|
||||
return (
|
||||
<Box padding={1} gap={0.5} display="flex" direction="column">
|
||||
<Box padding={1} gap={0.25} display="flex" direction="column">
|
||||
<DashboardOutlineNode sceneObject={dashboard} expandable />
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -40,34 +40,19 @@ function DashboardOutlineNode({ sceneObject, expandable }: { sceneObject: SceneO
|
|||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={0.5}
|
||||
alignItems="center"
|
||||
role="presentation"
|
||||
aria-expanded={expandable ? isExpanded : undefined}
|
||||
aria-owns={expandable ? key : undefined}
|
||||
<button
|
||||
role="treeitem"
|
||||
className={cx(styles.nodeButton, isCloned && styles.nodeButtonClone, isSelected && styles.nodeButtonSelected)}
|
||||
onPointerDown={(evt) => {
|
||||
onSelect?.(evt);
|
||||
setIsExpanded(!isExpanded);
|
||||
}}
|
||||
>
|
||||
{expandable && (
|
||||
<IconButton
|
||||
name={isExpanded ? 'angle-down' : 'angle-right'}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
aria-label={
|
||||
isExpanded
|
||||
? t('dashboard.outline.tree.item.collapse', 'Collapse item')
|
||||
: t('dashboard.outline.tree.item.expand', 'Expand item')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
role="treeitem"
|
||||
className={cx(styles.nodeButton, isCloned && styles.nodeButtonClone, isSelected && styles.nodeButtonSelected)}
|
||||
onPointerDown={(evt) => onSelect?.(evt)}
|
||||
>
|
||||
<Icon name={elementInfo.icon} />
|
||||
<span>{elementInfo.instanceName}</span>
|
||||
</button>
|
||||
</Stack>
|
||||
{expandable && <Icon name={isExpanded ? 'angle-down' : 'angle-right'} />}
|
||||
<Icon size="sm" name={elementInfo.icon} />
|
||||
<span>{elementInfo.instanceName}</span>
|
||||
</button>
|
||||
|
||||
{expandable && isExpanded && (
|
||||
<div className={styles.container} role="group">
|
||||
{children.length > 0 ? (
|
||||
|
|
@ -94,7 +79,7 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
gap: theme.spacing(0.5),
|
||||
marginLeft: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(1.5),
|
||||
borderLeft: `1px solid ${theme.colors.border.medium}`,
|
||||
|
|
@ -105,12 +90,16 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
background: 'transparent',
|
||||
padding: theme.spacing(0.25, 1),
|
||||
borderRadius: theme.shape.radius.default,
|
||||
color: theme.colors.text.secondary,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
gap: theme.spacing(0.5),
|
||||
overflow: 'hidden',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colors.action.hover,
|
||||
color: theme.colors.text.primary,
|
||||
outline: `1px dashed ${theme.colors.border.strong}`,
|
||||
outlineOffset: '0px',
|
||||
backgroundColor: theme.colors.emphasize(theme.colors.background.canvas, 0.08),
|
||||
},
|
||||
'> span': {
|
||||
whiteSpace: 'nowrap',
|
||||
|
|
@ -119,7 +108,12 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
},
|
||||
}),
|
||||
nodeButtonSelected: css({
|
||||
color: theme.colors.primary.text,
|
||||
color: theme.colors.text.primary,
|
||||
outline: `1px dashed ${theme.colors.primary.border}`,
|
||||
outlineOffset: '0px',
|
||||
'&:hover': {
|
||||
outline: `1px dashed ${theme.colors.primary.border}`,
|
||||
},
|
||||
}),
|
||||
nodeButtonClone: css({
|
||||
color: theme.colors.text.secondary,
|
||||
|
|
|
|||
|
|
@ -1,26 +1,43 @@
|
|||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Menu, Stack, Text, useStyles2, ConfirmButton, Dropdown, Icon } from '@grafana/ui';
|
||||
import { Button, Menu, Stack, Text, useStyles2, ConfirmButton, Dropdown, Icon, IconButton } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
|
||||
|
||||
import { DashboardEditPane } from './DashboardEditPane';
|
||||
|
||||
interface EditPaneHeaderProps {
|
||||
element: EditableDashboardElement;
|
||||
editPane: DashboardEditPane;
|
||||
}
|
||||
|
||||
export function EditPaneHeader({ element }: EditPaneHeaderProps) {
|
||||
export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) {
|
||||
const elementInfo = element.getEditableElementInfo();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const onCopy = element.onCopy?.bind(element);
|
||||
const onDuplicate = element.onDuplicate?.bind(element);
|
||||
const onDelete = element.onDelete?.bind(element);
|
||||
// temporary simple solution, should select parent element
|
||||
const onGoBack = () => editPane.clearSelection();
|
||||
const canGoBack = editPane.state.selection;
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Text variant="h5">{elementInfo.typeName}</Text>
|
||||
<Stack direction="row" gap={1}>
|
||||
{canGoBack && (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
size="lg"
|
||||
onClick={onGoBack}
|
||||
tooltip={t('grafana.dashboard.edit-pane.go-back', 'Go back')}
|
||||
aria-abel={t('grafana.dashboard.edit-pane.go-back', 'Go back')}
|
||||
/>
|
||||
)}
|
||||
<Text>{elementInfo.typeName}</Text>
|
||||
</Stack>
|
||||
<Stack direction="row" gap={1}>
|
||||
{element.renderActions && element.renderActions()}
|
||||
{(onCopy || onDelete) && (
|
||||
|
|
@ -80,7 +97,7 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(2),
|
||||
padding: theme.spacing(1, 2),
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,37 @@
|
|||
import { Stack } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { ScrollContainer, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
|
||||
|
||||
import { DashboardEditPane } from './DashboardEditPane';
|
||||
import { EditPaneHeader } from './EditPaneHeader';
|
||||
|
||||
export interface Props {
|
||||
element: EditableDashboardElement;
|
||||
editPane: DashboardEditPane;
|
||||
}
|
||||
|
||||
export function ElementEditPane({ element }: Props) {
|
||||
export function ElementEditPane({ element, editPane }: Props) {
|
||||
const categories = element.useEditPaneOptions ? element.useEditPaneOptions() : [];
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={0}>
|
||||
<EditPaneHeader element={element} />
|
||||
{categories.map((cat) => cat.render())}
|
||||
</Stack>
|
||||
<div className={styles.wrapper}>
|
||||
<EditPaneHeader element={element} editPane={editPane} />
|
||||
<ScrollContainer showScrollIndicators={true}>{categories.map((cat) => cat.render())}</ScrollContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1 1 0',
|
||||
height: '100%',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export class RowItem
|
|||
return {
|
||||
typeName: t('dashboard.edit-pane.elements.row', 'Row'),
|
||||
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
|
||||
icon: 'line-alt',
|
||||
icon: 'list-ul',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export class TabItem
|
|||
return {
|
||||
typeName: t('dashboard.edit-pane.elements.tab', 'Tab'),
|
||||
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
|
||||
icon: 'tag-alt',
|
||||
icon: 'layers',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { isLibraryPanel } from '../../utils/utils';
|
|||
import { DashboardScene } from '../DashboardScene';
|
||||
|
||||
import { BackToDashboardButton } from './actions/BackToDashboardButton';
|
||||
import { DashboardAddButton } from './actions/DashboardAddButton';
|
||||
import { DashboardSettingsButton } from './actions/DashboardSettingsButton';
|
||||
import { DiscardLibraryPanelButton } from './actions/DiscardLibraryPanelButton';
|
||||
import { DiscardPanelButton } from './actions/DiscardPanelButton';
|
||||
|
|
@ -98,6 +99,12 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
|
|||
group: 'panel',
|
||||
condition: showPanelButtons && isEditingLibraryPanel,
|
||||
},
|
||||
{
|
||||
key: 'dashboard-add-button',
|
||||
component: DashboardAddButton,
|
||||
group: 'dashboard',
|
||||
condition: isEditingAndShowingDashboard && dashboard.canEditDashboard(),
|
||||
},
|
||||
{
|
||||
key: 'edit-schema-v2-button',
|
||||
component: EditSchemaV2Button,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { ToolbarActionProps } from '../types';
|
||||
|
||||
export function DashboardAddButton({ dashboard }: ToolbarActionProps) {
|
||||
return (
|
||||
<Button
|
||||
tooltip={t('dashboard.toolbar.add-new.tooltip', 'Add panels and other elements')}
|
||||
icon="plus"
|
||||
onClick={() => dashboard.state.editPane.toggleAddPane()}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
fill="outline"
|
||||
data-testid={selectors.components.PageToolbar.itemButton('Add button')}
|
||||
>
|
||||
<Trans i18nKey="dashboard.toolbar.add">Add</Trans>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ export interface OptionsPaneCategoryProps {
|
|||
renderTitle?: (isExpanded: boolean) => React.ReactNode;
|
||||
isOpenDefault?: boolean;
|
||||
itemsCount?: number;
|
||||
forceOpen?: number;
|
||||
forceOpen?: boolean;
|
||||
className?: string;
|
||||
isNested?: boolean;
|
||||
children: ReactNode;
|
||||
|
|
@ -42,37 +42,24 @@ export const OptionsPaneCategory = React.memo(
|
|||
isExpanded: isOpenDefault,
|
||||
});
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(savedState?.isExpanded ?? isOpenDefault);
|
||||
const manualClickTime = useRef(0);
|
||||
const isExpandedInitialValue = forceOpen || (savedState?.isExpanded ?? isOpenDefault);
|
||||
const [isExpanded, setIsExpanded] = useState(isExpandedInitialValue);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [queryParams, updateQueryParams] = useQueryParams();
|
||||
const isOpenFromUrl = queryParams[CATEGORY_PARAM_NAME] === id;
|
||||
|
||||
// Handle opening by forceOpen param or from URL
|
||||
useEffect(() => {
|
||||
if (manualClickTime.current) {
|
||||
// ignore changes since the click handled the expected behavior
|
||||
if (Date.now() - manualClickTime.current < 200) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isOpenFromUrl || forceOpen) {
|
||||
if (!isExpanded) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
if (isOpenFromUrl) {
|
||||
if ((forceOpen || isOpenFromUrl) && !isExpanded) {
|
||||
setIsExpanded(true);
|
||||
setTimeout(() => {
|
||||
ref.current?.scrollIntoView();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}, [forceOpen, isExpanded, isOpenFromUrl]);
|
||||
}, [isExpanded, isOpenFromUrl, forceOpen]);
|
||||
|
||||
const onToggle = useCallback(() => {
|
||||
manualClickTime.current = Date.now();
|
||||
updateQueryParams(
|
||||
{
|
||||
[CATEGORY_PARAM_NAME]: isExpanded ? undefined : id,
|
||||
},
|
||||
true
|
||||
);
|
||||
updateQueryParams({ [CATEGORY_PARAM_NAME]: isExpanded ? undefined : id }, true);
|
||||
setSavedState({ isExpanded: !isExpanded });
|
||||
setIsExpanded(!isExpanded);
|
||||
}, [updateQueryParams, isExpanded, id, setSavedState]);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export interface OptionsPaneCategoryDescriptorProps {
|
|||
title: string;
|
||||
renderTitle?: (isExpanded: boolean) => React.ReactNode;
|
||||
isOpenDefault?: boolean;
|
||||
forceOpen?: number;
|
||||
forceOpen?: boolean;
|
||||
className?: string;
|
||||
isNested?: boolean;
|
||||
itemsCount?: number;
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const OptionsPaneOptions = (props: OptionPaneRenderProps) => {
|
|||
break;
|
||||
case OptionFilter.Recent:
|
||||
mainBoxElements.push(
|
||||
<OptionsPaneCategory id="Recent options" title="Recent options" key="Recent options" forceOpen={1}>
|
||||
<OptionsPaneCategory id="Recent options" title="Recent options" key="Recent options" forceOpen={true}>
|
||||
{getRecentOptions(allOptions).map((item) => item.render())}
|
||||
</OptionsPaneCategory>
|
||||
);
|
||||
|
|
@ -163,7 +163,7 @@ export function renderSearchHits(
|
|||
id="Found options"
|
||||
title={`Matched ${optionHits.length}/${totalCount} options`}
|
||||
key="Normal options"
|
||||
forceOpen={1}
|
||||
forceOpen={true}
|
||||
>
|
||||
{optionHits.map((hit) => hit.render(searchQuery))}
|
||||
</OptionsPaneCategory>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export function getFieldOverrideCategories(
|
|||
const configPropertiesOptions = getOverrideProperties(registry);
|
||||
const isSystemOverride = isSystemOverrideGuard(override);
|
||||
// A way to force open new override categories
|
||||
const forceOpen = override.properties.length === 0 ? 1 : 0;
|
||||
const forceOpen = override.properties.length === 0;
|
||||
|
||||
const category = new OptionsPaneCategoryDescriptor({
|
||||
title: overrideName,
|
||||
|
|
|
|||
|
|
@ -1395,7 +1395,8 @@
|
|||
"tab": {
|
||||
"heading": "Tab",
|
||||
"title": "Break up your dashboard into different horizontal tabs"
|
||||
}
|
||||
},
|
||||
"title": "Add element"
|
||||
},
|
||||
"elements": {
|
||||
"dashboard": "Dashboard",
|
||||
|
|
@ -1415,11 +1416,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editpane": {
|
||||
"add": "Add",
|
||||
"configure": "Configure",
|
||||
"outline": "Outline"
|
||||
},
|
||||
"empty": {
|
||||
"add-library-panel-body": "Add visualizations that are shared with other dashboards.",
|
||||
"add-library-panel-button": "Add library panel",
|
||||
|
|
@ -1503,9 +1499,7 @@
|
|||
"outline": {
|
||||
"tree": {
|
||||
"item": {
|
||||
"collapse": "Collapse item",
|
||||
"empty": "(empty)",
|
||||
"expand": "Expand item"
|
||||
"empty": "(empty)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -1607,6 +1601,9 @@
|
|||
},
|
||||
"toolbar": {
|
||||
"add": "Add",
|
||||
"add-new": {
|
||||
"tooltip": "Add panels and other elements"
|
||||
},
|
||||
"alert-rules": "Alert rules",
|
||||
"back-to-dashboard": "Back to dashboard",
|
||||
"dashboard-settings": {
|
||||
|
|
@ -2409,6 +2406,13 @@
|
|||
"requires-license": "Requires a Grafana Enterprise license",
|
||||
"title": "Enterprise"
|
||||
},
|
||||
"grafana": {
|
||||
"dashboard": {
|
||||
"edit-pane": {
|
||||
"go-back": "Go back"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grafana-ui": {
|
||||
"action-editor": {
|
||||
"button": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue