|
|
|
@ -4,7 +4,7 @@ import React, { useEffect, useMemo, useState } from 'react'; |
|
|
|
|
|
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data'; |
|
|
|
|
import { SceneObject } from '@grafana/scenes'; |
|
|
|
|
import { Box, Icon, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui'; |
|
|
|
|
import { Box, Icon, Text, useElementSelection, useStyles2, useTheme2 } from '@grafana/ui'; |
|
|
|
|
import { t, Trans } from 'app/core/internationalization'; |
|
|
|
|
|
|
|
|
|
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; |
|
|
|
@ -24,7 +24,7 @@ export function DashboardOutline({ editPane }: Props) { |
|
|
|
|
const dashboard = getDashboardSceneFor(editPane); |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<Box padding={1} gap={0.25} display="flex" direction="column"> |
|
|
|
|
<Box padding={1} gap={0} display="flex" direction="column"> |
|
|
|
|
<DashboardOutlineNode sceneObject={dashboard} editPane={editPane} depth={0} /> |
|
|
|
|
</Box> |
|
|
|
|
); |
|
|
|
@ -40,6 +40,7 @@ function DashboardOutlineNode({ |
|
|
|
|
depth: number; |
|
|
|
|
}) { |
|
|
|
|
const [isCollapsed, setIsCollapsed] = useState(depth > 0); |
|
|
|
|
const theme = useTheme2(); |
|
|
|
|
const { key } = sceneObject.useState(); |
|
|
|
|
const styles = useStyles2(getStyles); |
|
|
|
|
const { isSelected, onSelect } = useElementSelection(key); |
|
|
|
@ -53,7 +54,7 @@ function DashboardOutlineNode({ |
|
|
|
|
const elementCollapsed = editableElement.getCollapsedState?.(); |
|
|
|
|
const outlineRename = useOutlineRename(editableElement); |
|
|
|
|
|
|
|
|
|
const onNameClicked = (evt: React.PointerEvent) => { |
|
|
|
|
const onNodeClicked = (evt: React.PointerEvent) => { |
|
|
|
|
// Only select via clicking outline never deselect
|
|
|
|
|
if (!isSelected) { |
|
|
|
|
onSelect?.(evt); |
|
|
|
@ -62,7 +63,8 @@ function DashboardOutlineNode({ |
|
|
|
|
editableElement.scrollIntoView?.(); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const onToggleCollapse = () => { |
|
|
|
|
const onToggleCollapse = (evt: React.MouseEvent) => { |
|
|
|
|
evt.stopPropagation(); |
|
|
|
|
setIsCollapsed(!isCollapsed); |
|
|
|
|
|
|
|
|
|
// Sync expanded state with canvas element
|
|
|
|
@ -80,16 +82,19 @@ function DashboardOutlineNode({ |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<> |
|
|
|
|
<Stack gap={0.5}> |
|
|
|
|
<div |
|
|
|
|
className={cx(styles.container, isSelected && styles.containerSelected)} |
|
|
|
|
style={{ paddingLeft: theme.spacing(depth * 3) }} |
|
|
|
|
onPointerDown={onNodeClicked} |
|
|
|
|
> |
|
|
|
|
{elementInfo.isContainer && ( |
|
|
|
|
<button role="treeitem" className={styles.angleButton} onClick={onToggleCollapse}> |
|
|
|
|
<button role="treeitem" className={styles.angleButton} onPointerDown={onToggleCollapse}> |
|
|
|
|
<Icon name={!isCollapsed ? 'angle-down' : 'angle-right'} /> |
|
|
|
|
</button> |
|
|
|
|
)} |
|
|
|
|
<button |
|
|
|
|
role="button" |
|
|
|
|
className={cx(styles.nodeButton, isCloned && styles.nodeButtonClone, isSelected && styles.nodeButtonSelected)} |
|
|
|
|
onPointerDown={onNameClicked} |
|
|
|
|
className={cx(styles.nodeName, isCloned && styles.nodeNameClone)} |
|
|
|
|
onDoubleClick={outlineRename.onNameDoubleClicked} |
|
|
|
|
> |
|
|
|
|
<Icon size="sm" name={elementInfo.icon} /> |
|
|
|
@ -111,10 +116,11 @@ function DashboardOutlineNode({ |
|
|
|
|
</> |
|
|
|
|
)} |
|
|
|
|
</button> |
|
|
|
|
</Stack> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
{elementInfo.isContainer && !isCollapsed && ( |
|
|
|
|
<div className={styles.container} role="group"> |
|
|
|
|
<div className={styles.nodeChildren}> |
|
|
|
|
<div className={styles.nodeChildrenLine} style={{ marginLeft: theme.spacing(depth * 3) }} /> |
|
|
|
|
{children.length > 0 ? ( |
|
|
|
|
children.map((child) => ( |
|
|
|
|
<DashboardOutlineNode |
|
|
|
@ -139,11 +145,29 @@ function getStyles(theme: GrafanaTheme2) { |
|
|
|
|
return { |
|
|
|
|
container: css({ |
|
|
|
|
display: 'flex', |
|
|
|
|
flexDirection: 'column', |
|
|
|
|
gap: theme.spacing(0.5), |
|
|
|
|
marginLeft: theme.spacing(1), |
|
|
|
|
paddingLeft: theme.spacing(1.5), |
|
|
|
|
borderLeft: `1px solid ${theme.colors.border.medium}`, |
|
|
|
|
alignItems: 'center', |
|
|
|
|
flexGrow: 1, |
|
|
|
|
borderRadius: theme.shape.radius.default, |
|
|
|
|
position: 'relative', |
|
|
|
|
marginBottom: theme.spacing(0.25), |
|
|
|
|
color: theme.colors.text.secondary, |
|
|
|
|
'&:hover': { |
|
|
|
|
color: theme.colors.text.primary, |
|
|
|
|
outline: `1px dashed ${theme.colors.border.strong}`, |
|
|
|
|
outlineOffset: '0px', |
|
|
|
|
backgroundColor: theme.colors.emphasize(theme.colors.background.primary, 0.05), |
|
|
|
|
}, |
|
|
|
|
}), |
|
|
|
|
containerSelected: css({ |
|
|
|
|
outline: `1px dashed ${theme.colors.primary.border} !important`, |
|
|
|
|
outlineOffset: '0px', |
|
|
|
|
color: theme.colors.text.primary, |
|
|
|
|
|
|
|
|
|
'&:hover': { |
|
|
|
|
outline: `1px dashed ${theme.colors.primary.border}`, |
|
|
|
|
color: theme.colors.text.primary, |
|
|
|
|
}, |
|
|
|
|
}), |
|
|
|
|
angleButton: css({ |
|
|
|
|
boxShadow: 'none', |
|
|
|
@ -151,57 +175,58 @@ function getStyles(theme: GrafanaTheme2) { |
|
|
|
|
background: 'transparent', |
|
|
|
|
borderRadius: theme.shape.radius.default, |
|
|
|
|
padding: 0, |
|
|
|
|
color: theme.colors.text.secondary, |
|
|
|
|
color: 'inherit', |
|
|
|
|
lineHeight: 0, |
|
|
|
|
}), |
|
|
|
|
nodeButton: css({ |
|
|
|
|
nodeName: css({ |
|
|
|
|
boxShadow: 'none', |
|
|
|
|
border: 'none', |
|
|
|
|
background: 'transparent', |
|
|
|
|
padding: theme.spacing(0.25, 1, 0.25, 0), |
|
|
|
|
borderRadius: theme.shape.radius.default, |
|
|
|
|
color: theme.colors.text.secondary, |
|
|
|
|
color: 'inherit', |
|
|
|
|
display: 'flex', |
|
|
|
|
flexGrow: 1, |
|
|
|
|
alignItems: 'center', |
|
|
|
|
gap: theme.spacing(0.5), |
|
|
|
|
overflow: 'hidden', |
|
|
|
|
'&: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', |
|
|
|
|
overflow: 'hidden', |
|
|
|
|
textOverflow: 'ellipsis', |
|
|
|
|
}, |
|
|
|
|
}), |
|
|
|
|
nodeButtonSelected: css({ |
|
|
|
|
color: theme.colors.text.primary, |
|
|
|
|
outline: `1px dashed ${theme.colors.primary.border} !important`, |
|
|
|
|
outlineOffset: '0px', |
|
|
|
|
'&:hover': { |
|
|
|
|
outline: `1px dashed ${theme.colors.primary.border}`, |
|
|
|
|
}, |
|
|
|
|
}), |
|
|
|
|
hiddenIcon: css({ |
|
|
|
|
color: theme.colors.text.secondary, |
|
|
|
|
marginLeft: theme.spacing(1), |
|
|
|
|
}), |
|
|
|
|
nodeButtonClone: css({ |
|
|
|
|
nodeNameClone: css({ |
|
|
|
|
color: theme.colors.text.secondary, |
|
|
|
|
cursor: 'not-allowed', |
|
|
|
|
}), |
|
|
|
|
outlineInput: css({ |
|
|
|
|
border: `1px solid ${theme.colors.primary.border}`, |
|
|
|
|
border: `1px solid ${theme.components.input.borderColor}`, |
|
|
|
|
height: theme.spacing(3), |
|
|
|
|
borderRadius: theme.shape.radius.default, |
|
|
|
|
|
|
|
|
|
'&:focus': { |
|
|
|
|
outline: 'none', |
|
|
|
|
boxShadow: 'none', |
|
|
|
|
}, |
|
|
|
|
}), |
|
|
|
|
nodeChildren: css({ |
|
|
|
|
display: 'flex', |
|
|
|
|
flexDirection: 'column', |
|
|
|
|
position: 'relative', |
|
|
|
|
}), |
|
|
|
|
nodeChildrenLine: css({ |
|
|
|
|
position: 'absolute', |
|
|
|
|
width: '1px', |
|
|
|
|
height: '100%', |
|
|
|
|
left: '7px', |
|
|
|
|
zIndex: 1, |
|
|
|
|
backgroundColor: theme.colors.border.weak, |
|
|
|
|
}), |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|