Dashboards: Rows layout polish and fixes (#102666)

pull/102707/head
Torkel Ödegaard 3 months ago committed by GitHub
parent c20de2b753
commit f0db0c4f0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts
  2. 10
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  3. 9
      public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemRenderer.tsx
  4. 6
      public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx
  5. 18
      public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx
  6. 4
      public/app/features/dashboard-scene/scene/layout-rows/RowItemMenu.tsx
  7. 49
      public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx
  8. 12
      public/app/features/dashboard-scene/utils/utils.ts
  9. 7
      public/locales/en-US/grafana.json

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

@ -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"
},

Loading…
Cancel
Save