Dashboard: Style change to hover and selected nodes in outline (#104462)
* Dashboard: Style change to hover and selected nodes in outline * Update * Update
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user