Dashboard: Style change to hover and selected nodes in outline (#104462)

* Dashboard: Style change to hover and selected nodes in outline

* Update

* Update
pull/103055/head
Torkel Ödegaard 3 weeks ago committed by GitHub
parent d3038c6e9a
commit 90e1f24510
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 91
      public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx
  2. 4
      public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx

@ -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,
}),
};
}

@ -24,7 +24,8 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa
new OptionsPaneCategoryDescriptor({ title: '', id: 'row-options' })
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.row-options.row.title', 'Title'),
title: '',
skipField: true,
render: () => <RowTitleInput row={model} isNewElement={isNewElement} />,
})
)
@ -85,6 +86,7 @@ function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: bool
return (
<Field
label={t('dashboard.rows-layout.row-options.row.title', 'Title')}
invalid={!hasUniqueTitle}
error={
!hasUniqueTitle ? t('dashboard.rows-layout.row-options.title-not-unique', 'Title should be unique') : undefined

Loading…
Cancel
Save