Dashboards: Rows layout polish and fixes (#102666)
This commit is contained in:
@@ -90,6 +90,14 @@ export function getDashboardGridStyles(theme: GrafanaTheme2) {
|
||||
},
|
||||
},
|
||||
|
||||
'.dashboard-canvas-add-button': {
|
||||
opacity: 0,
|
||||
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
|
||||
'.dashboard-visible-hidden-element': {
|
||||
opacity: 0.6,
|
||||
|
||||
|
||||
@@ -108,8 +108,6 @@ export interface DashboardSceneState extends SceneObjectState {
|
||||
controls?: DashboardControls;
|
||||
/** True when editing */
|
||||
isEditing?: boolean;
|
||||
/** Controls the visibility of hidden elements like row headers */
|
||||
showHiddenElements?: boolean;
|
||||
/** True when user made a change */
|
||||
isDirty?: boolean;
|
||||
/** meta flags */
|
||||
@@ -270,7 +268,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
this._initialUrlState = locationService.getLocation();
|
||||
|
||||
// Switch to edit mode
|
||||
this.setState({ isEditing: true, showHiddenElements: true });
|
||||
this.setState({ isEditing: true });
|
||||
|
||||
// Propagate change edit mode change to children
|
||||
this.state.body.editModeChanged?.(true);
|
||||
@@ -355,10 +353,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
|
||||
if (restoreInitialState) {
|
||||
// Restore initial state and disable editing
|
||||
this.setState({ ...this._initialState, isEditing: false, showHiddenElements: false });
|
||||
this.setState({ ...this._initialState, isEditing: false });
|
||||
} else {
|
||||
// Do not restore
|
||||
this.setState({ isEditing: false, showHiddenElements: false });
|
||||
this.setState({ isEditing: false });
|
||||
}
|
||||
|
||||
// if we are in edit panel, we need to onDiscard()
|
||||
@@ -376,8 +374,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
return this._initialState !== undefined;
|
||||
}
|
||||
|
||||
public onToggleHiddenElements = () => this.setState({ showHiddenElements: !this.state.showHiddenElements });
|
||||
|
||||
public pauseTrackingChanges() {
|
||||
this._changeTracker.stopTrackingChanges();
|
||||
}
|
||||
|
||||
+4
-5
@@ -10,24 +10,23 @@ export interface ResponsiveGridItemProps extends SceneComponentProps<ResponsiveG
|
||||
|
||||
export function ResponsiveGridItemRenderer({ model }: ResponsiveGridItemProps) {
|
||||
const { body } = model.useState();
|
||||
const { showHiddenElements } = useDashboardState(model);
|
||||
const { isEditing } = useDashboardState(model);
|
||||
const isConditionallyHidden = useIsConditionallyHidden(model);
|
||||
|
||||
if (isConditionallyHidden && !showHiddenElements) {
|
||||
if (isConditionallyHidden && !isEditing) {
|
||||
return null;
|
||||
}
|
||||
const isHiddenButVisibleElement = showHiddenElements && isConditionallyHidden;
|
||||
|
||||
return model.state.repeatedPanels ? (
|
||||
<>
|
||||
{model.state.repeatedPanels.map((item) => (
|
||||
<div className={cx({ 'dashboard-visible-hidden-element': isHiddenButVisibleElement })} key={item.state.key}>
|
||||
<div className={cx({ 'dashboard-visible-hidden-element': isConditionallyHidden })} key={item.state.key}>
|
||||
<item.Component model={item} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className={cx({ 'dashboard-visible-hidden-element': isHiddenButVisibleElement })}>
|
||||
<div className={cx({ 'dashboard-visible-hidden-element': isConditionallyHidden })}>
|
||||
<body.Component model={body} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface RowItemState extends SceneObjectState {
|
||||
title?: string;
|
||||
isCollapsed?: boolean;
|
||||
isHeaderHidden?: boolean;
|
||||
height?: 'expand' | 'min';
|
||||
fillScreen?: boolean;
|
||||
conditionalRendering?: ConditionalRendering;
|
||||
}
|
||||
|
||||
@@ -141,8 +141,8 @@ export class RowItem
|
||||
this.setState({ isHeaderHidden });
|
||||
}
|
||||
|
||||
public onChangeHeight(height: 'expand' | 'min') {
|
||||
this.setState({ height });
|
||||
public onChangeFillScreen(fillScreen: boolean) {
|
||||
this.setState({ fillScreen });
|
||||
}
|
||||
|
||||
public onChangeRepeat(repeat: string | undefined) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Alert, Input, RadioButtonGroup, Switch, TextLink } from '@grafana/ui';
|
||||
import { Alert, Input, Switch, TextLink } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
@@ -31,8 +30,8 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.row.height', 'Height'),
|
||||
render: () => <RowHeightSelect row={model} />,
|
||||
title: t('dashboard.rows-layout.row-options.row.fill-screen', 'Fill screen'),
|
||||
render: () => <FillScreenSwitch row={model} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
@@ -99,15 +98,10 @@ function RowHeaderSwitch({ row }: { row: RowItem }) {
|
||||
return <Switch value={isHeaderHidden} onChange={() => row.onHeaderHiddenToggle()} />;
|
||||
}
|
||||
|
||||
function RowHeightSelect({ row }: { row: RowItem }) {
|
||||
const { height = 'min' } = row.useState();
|
||||
function FillScreenSwitch({ row }: { row: RowItem }) {
|
||||
const { fillScreen } = row.useState();
|
||||
|
||||
const options: Array<SelectableValue<'expand' | 'min'>> = [
|
||||
{ label: t('dashboard.rows-layout.options.height-expand', 'Expand'), value: 'expand' },
|
||||
{ label: t('dashboard.rows-layout.options.height-min', 'Min'), value: 'min' },
|
||||
];
|
||||
|
||||
return <RadioButtonGroup options={options} value={height} onChange={(option) => row.onChangeHeight(option)} />;
|
||||
return <Switch value={fillScreen} onChange={() => row.onChangeFillScreen(!fillScreen)} />;
|
||||
}
|
||||
|
||||
function RowRepeatSelect({ row }: { row: RowItem }) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Dropdown, Menu, ToolbarButtonRow, useStyles2 } from '@grafana/ui';
|
||||
@@ -14,7 +14,7 @@ export function RowItemMenu({ model }: RowItemMenuProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<ToolbarButtonRow className={styles.container}>
|
||||
<ToolbarButtonRow className={cx(styles.container, 'dashboard-canvas-add-button')}>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
overlay={() => (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useState } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { clearButtonStyles, Icon, useStyles2 } from '@grafana/ui';
|
||||
import { clearButtonStyles, Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { useIsClone } from '../../utils/clone';
|
||||
@@ -19,25 +19,24 @@ import { RowItem } from './RowItem';
|
||||
import { RowItemMenu } from './RowItemMenu';
|
||||
|
||||
export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
||||
const { layout, isCollapsed, height = 'min', isHeaderHidden } = model.useState();
|
||||
const { layout, isCollapsed, fillScreen, isHeaderHidden } = model.useState();
|
||||
const isClone = useIsClone(model);
|
||||
const { isEditing, showHiddenElements } = useDashboardState(model);
|
||||
const { isEditing } = useDashboardState(model);
|
||||
const isConditionallyHidden = useIsConditionallyHidden(model);
|
||||
const { isSelected, onSelect, isSelectable } = useElementSelectionScene(model);
|
||||
const title = useInterpolatedTitle(model);
|
||||
const styles = useStyles2(getStyles);
|
||||
const clearStyles = useStyles2(clearButtonStyles);
|
||||
|
||||
const shouldGrow = !isCollapsed && height === 'expand';
|
||||
const isHiddenButVisibleElement = showHiddenElements && isConditionallyHidden;
|
||||
const isHiddenButVisibleHeader = showHiddenElements && isHeaderHidden;
|
||||
const shouldGrow = !isCollapsed && fillScreen;
|
||||
const isHidden = isConditionallyHidden && !isEditing;
|
||||
|
||||
// Highlight the full row when hovering over header
|
||||
const [selectableHighlight, setSelectableHighlight] = useState(false);
|
||||
const onHeaderEnter = useCallback(() => setSelectableHighlight(true), []);
|
||||
const onHeaderLeave = useCallback(() => setSelectableHighlight(false), []);
|
||||
|
||||
if (isConditionallyHidden && !showHiddenElements) {
|
||||
if (isHidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -49,19 +48,24 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
||||
isEditing && isCollapsed && styles.wrapperEditingCollapsed,
|
||||
isCollapsed && styles.wrapperCollapsed,
|
||||
shouldGrow && styles.wrapperGrow,
|
||||
isHiddenButVisibleElement && 'dashboard-visible-hidden-element',
|
||||
isConditionallyHidden && 'dashboard-visible-hidden-element',
|
||||
!isClone && isSelected && 'dashboard-selected-element',
|
||||
!isClone && !isSelected && selectableHighlight && 'dashboard-selectable-element'
|
||||
)}
|
||||
onPointerDown={onSelect}
|
||||
onPointerDown={(e) => {
|
||||
// If we selected and are clicking a button inside row header then don't de-select row
|
||||
if (isSelected && e.target instanceof Element && e.target.closest('button')) {
|
||||
// Stop propagation otherwise dashboaed level onPointerDown will de-select row
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect?.(e);
|
||||
}}
|
||||
>
|
||||
{(!isHeaderHidden || (isEditing && showHiddenElements)) && (
|
||||
{(!isHeaderHidden || isEditing) && (
|
||||
<div
|
||||
className={cx(
|
||||
isHiddenButVisibleHeader && 'dashboard-visible-hidden-element',
|
||||
styles.rowHeader,
|
||||
'dashboard-row-header'
|
||||
)}
|
||||
className={cx(isHeaderHidden && 'dashboard-visible-hidden-element', styles.rowHeader, 'dashboard-row-header')}
|
||||
onMouseEnter={isSelectable ? onHeaderEnter : undefined}
|
||||
onMouseLeave={isSelectable ? onHeaderLeave : undefined}
|
||||
>
|
||||
@@ -76,8 +80,15 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
||||
data-testid={selectors.components.DashboardRow.title(title!)}
|
||||
>
|
||||
<Icon name={isCollapsed ? 'angle-right' : 'angle-down'} />
|
||||
<span className={styles.rowTitle} role="heading">
|
||||
<span className={cx(styles.rowTitle, isHeaderHidden && styles.rowTitleHidden)} role="heading">
|
||||
{title}
|
||||
{isHeaderHidden && (
|
||||
<Tooltip
|
||||
content={t('dashboard.rows-layout.header-hidden-tooltip', 'Row header only visible in edit mode')}
|
||||
>
|
||||
<Icon name="eye-slash" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
{!isClone && isEditing && <RowItemMenu model={model} />}
|
||||
@@ -108,6 +119,9 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
rowTitle: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(2),
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
whiteSpace: 'nowrap',
|
||||
@@ -117,6 +131,9 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
flexGrow: 1,
|
||||
minWidth: 0,
|
||||
}),
|
||||
rowTitleHidden: css({
|
||||
textDecoration: 'line-through',
|
||||
}),
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
@@ -446,17 +446,9 @@ export function useDashboard(scene: SceneObject): DashboardScene {
|
||||
return getDashboardSceneFor(scene);
|
||||
}
|
||||
|
||||
export function useDashboardState(
|
||||
scene: SceneObject
|
||||
): DashboardSceneState & { isEditing: boolean; showHiddenElements: boolean } {
|
||||
export function useDashboardState(scene: SceneObject): DashboardSceneState {
|
||||
const dashboard = useDashboard(scene);
|
||||
const state = dashboard.useState();
|
||||
|
||||
return {
|
||||
...state,
|
||||
isEditing: !!state.isEditing,
|
||||
showHiddenElements: !!(state.isEditing && state.showHiddenElements),
|
||||
};
|
||||
return dashboard.useState();
|
||||
}
|
||||
|
||||
export function useIsConditionallyHidden(scene: RowItem | ResponsiveGridItem): boolean {
|
||||
|
||||
@@ -1539,11 +1539,8 @@
|
||||
},
|
||||
"rows-layout": {
|
||||
"description": "Collapsable panel groups with headings",
|
||||
"header-hidden-tooltip": "Row header only visible in edit mode",
|
||||
"name": "Rows",
|
||||
"options": {
|
||||
"height-expand": "Expand",
|
||||
"height-min": "Min"
|
||||
},
|
||||
"row": {
|
||||
"collapse": "Collapse row",
|
||||
"expand": "Expand row",
|
||||
@@ -1571,7 +1568,7 @@
|
||||
}
|
||||
},
|
||||
"row": {
|
||||
"height": "Height",
|
||||
"fill-screen": "Fill screen",
|
||||
"hide-header": "Hide row header",
|
||||
"title": "Title"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user