Dynamic Dashboards: Fix some of the issues identified during bugbash (#102279)

This commit is contained in:
Bogdan Matei
2025-03-18 14:34:14 +02:00
committed by GitHub
parent c5b48285b6
commit 621e7012df
10 changed files with 175 additions and 88 deletions
@@ -45,6 +45,12 @@ export function useElementSelection(id: string | undefined): UseElementSelection
// To prevent this click form clearing the selection
evt.stopPropagation();
// Prevent text selection caused by shift click
if (evt.shiftKey) {
evt.preventDefault();
window.getSelection()?.empty();
}
context.onSelect({ id }, evt.shiftKey);
},
[context, id]
@@ -24,10 +24,11 @@ export interface TabProps extends HTMLProps<HTMLElement> {
counter?: number | null;
/** Extra content, displayed after the tab label and counter */
suffix?: NavModelItem['tabSuffix'];
truncate?: boolean;
}
export const Tab = React.forwardRef<HTMLElement, TabProps>(
({ label, active, icon, onChangeTab, counter, suffix: Suffix, className, href, ...otherProps }, ref) => {
({ label, active, icon, onChangeTab, counter, suffix: Suffix, className, href, truncate, ...otherProps }, ref) => {
const tabsStyles = useStyles2(getStyles);
const clearStyles = useStyles2(clearButtonStyles);
@@ -40,7 +41,12 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
</>
);
const linkClass = cx(clearStyles, tabsStyles.link, active ? tabsStyles.activeStyle : tabsStyles.notActive);
const linkClass = cx(
clearStyles,
tabsStyles.link,
active ? tabsStyles.activeStyle : tabsStyles.notActive,
truncate && tabsStyles.linkTruncate
);
const commonProps = {
className: linkClass,
@@ -53,7 +59,7 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
if (href) {
return (
<div className={cx(tabsStyles.item, className)}>
<div className={cx(tabsStyles.item, truncate && tabsStyles.itemTruncate, className)}>
<a
{...commonProps}
href={href}
@@ -68,7 +74,7 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
}
return (
<div className={cx(tabsStyles.item, className)}>
<div className={cx(tabsStyles.item, truncate && tabsStyles.itemTruncate, className)}>
<button
{...commonProps}
type="button"
@@ -94,6 +100,9 @@ const getStyles = (theme: GrafanaTheme2) => {
whiteSpace: 'nowrap',
padding: theme.spacing(0, 0.5),
}),
itemTruncate: css({
maxWidth: theme.spacing(40),
}),
link: css({
color: theme.colors.text.secondary,
padding: theme.spacing(1, 1.5, 1),
@@ -119,6 +128,12 @@ const getStyles = (theme: GrafanaTheme2) => {
bottom: 0,
},
}),
linkTruncate: css({
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
wordBreak: 'break-word',
overflow: 'hidden',
}),
notActive: css({
'a:hover, &:hover, &:focus': {
color: theme.colors.text.primary,
@@ -1,9 +1,10 @@
import { css } from '@emotion/css';
import { useEffect, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Box, Card, Icon } from '@grafana/ui';
import { Box, Card, Icon, IconName, useStyles2 } from '@grafana/ui';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { t, Trans } from 'app/core/internationalization';
import { t } from 'app/core/internationalization';
import store from 'app/core/store';
import { DashboardInteractions } from '../utils/interactions';
@@ -15,7 +16,17 @@ export interface Props {
editPane: DashboardEditPane;
}
interface CardConfig {
icon: IconName;
heading: string;
title: string;
testId: string;
onClick: () => void;
hide?: boolean;
}
export function DashboardAddPane({ editPane }: Props) {
const styles = useStyles2(getStyles);
const dashboard = getDashboardSceneFor(editPane);
const [hasCopiedPanel, setHasCopiedPanel] = useState(store.exists(LS_PANEL_COPY_KEY));
@@ -27,76 +38,69 @@ export function DashboardAddPane({ editPane }: Props) {
return () => unsubscribe();
}, []);
const cards: CardConfig[] = [
{
icon: 'graph-bar',
heading: t('dashboard.edit-pane.add.panel.heading', 'Panel'),
title: t('dashboard.edit-pane.add.panel.title', 'A container for visualizations and other content'),
testId: selectors.components.PageToolbar.itemButton('add_visualization'),
onClick: () => dashboard.onCreateNewPanel(),
},
{
icon: 'import',
heading: t('dashboard.edit-pane.add.lib-panel.heading', 'Library panel'),
title: t(
'dashboard.edit-pane.add.lib-panel.title',
'Library panels allow you share and reuse panels between dashboards'
),
testId: selectors.pages.AddDashboard.itemButton('Add new panel from panel library menu item'),
onClick: () => {
dashboard.onShowAddLibraryPanelDrawer();
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' });
},
},
{
icon: 'list-ul',
heading: t('dashboard.edit-pane.add.row.heading', 'Row'),
title: t('dashboard.edit-pane.add.row.title', 'A group of panels with an optional header'),
testId: selectors.components.PageToolbar.itemButton('add_row'),
onClick: () => dashboard.onCreateNewRow(),
},
{
icon: 'layer-group',
heading: t('dashboard.edit-pane.add.tab.heading', 'Tab'),
title: t('dashboard.edit-pane.add.tab.title', 'Break up your dashboard into different horizontal tabs'),
testId: selectors.components.PageToolbar.itemButton('add_tab'),
onClick: () => dashboard.onCreateNewTab(),
},
{
hide: !hasCopiedPanel,
icon: 'clipboard-alt',
heading: t('dashboard.edit-pane.add.paste-panel.heading', 'Paste panel'),
title: t('dashboard.edit-pane.add.paste-panel.title', 'Paste a panel from the clipboard'),
testId: selectors.components.PageToolbar.itemButton('paste_panel'),
onClick: () => dashboard.pastePanel(),
},
];
return (
<Box display={'flex'} direction={'column'} gap={1} padding={2}>
<Card
onClick={() => dashboard.onCreateNewPanel()}
data-testid={selectors.components.PageToolbar.itemButton('add_visualization')}
title={t('dashboard.toolbar.add-panel-description', 'A container for visualizations and other content')}
>
<Card.Heading>
<Trans i18nKey="dashboard.toolbar.add-panel">Panel</Trans>
</Card.Heading>
<Card.Figure>
<Icon name="graph-bar" size="xl" />
</Card.Figure>
</Card>
<Card
onClick={() => {
dashboard.onShowAddLibraryPanelDrawer();
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' });
}}
data-testid={selectors.pages.AddDashboard.itemButton('Add new panel from panel library menu item')}
title={t(
'dashboard.toolbar.libray-panel-description',
'Libray panels allow you share and reuse panels between dashboards'
)}
>
<Card.Heading>
<Trans i18nKey="dashboard.toolbar.add-panel-lib">Import library panel</Trans>
</Card.Heading>
<Card.Figure>
<Icon name="import" size="xl" />
</Card.Figure>
</Card>
<Card
onClick={() => dashboard.onCreateNewRow()}
data-testid={selectors.components.PageToolbar.itemButton('add_row')}
title={t('dashboard.toolbar.row-description', 'A group of panels with an optional header')}
>
<Card.Heading>
<Trans i18nKey="dashboard.toolbar.add-row">Row</Trans>
</Card.Heading>
<Card.Figure>
<Icon name="list-ul" size="xl" />
</Card.Figure>
</Card>
<Card
onClick={() => dashboard.onCreateNewTab()}
data-testid={selectors.components.PageToolbar.itemButton('add_tab')}
title={t('dashboard.toolbar.tabs-description', 'Break up your dashboard into different horizontal tabs')}
>
<Card.Heading>
<Trans i18nKey="dashboard.toolbar.add-tab">Tab</Trans>
</Card.Heading>
<Card.Figure>
<Icon name="layer-group" size="xl" />
</Card.Figure>
</Card>
{hasCopiedPanel && (
<Card
onClick={() => dashboard.pastePanel()}
data-testid={selectors.components.PageToolbar.itemButton('paste_panel')}
title={t('dashboard.toolbar.paste-panel-description', 'Paste a panel from the clipboard')}
>
<Card.Heading>
<Trans i18nKey="dashboard.toolbar.paste-panel">Paste panel</Trans>
</Card.Heading>
<Card.Figure>
<Icon name="clipboard-alt" size="xl" />
</Card.Figure>
</Card>
<Box display="flex" direction="column" gap={1} padding={2}>
{cards.map(({ icon, heading, title, testId, onClick, hide }) =>
hide ? null : (
<Card onClick={onClick} data-testid={testId} title={title} key={title}>
<Card.Heading>{heading}</Card.Heading>
<Card.Figure className={styles.figure}>
<Icon name={icon} size="xl" />
</Card.Figure>
</Card>
)
)}
</Box>
);
}
const getStyles = () => ({
figure: css({
pointerEvents: 'none',
}),
});
@@ -21,7 +21,7 @@ import { DashboardAddPane } from './DashboardAddPane';
import { DashboardOutline } from './DashboardOutline';
import { ElementEditPane } from './ElementEditPane';
import { ElementSelection } from './ElementSelection';
import { NewObjectAddedToCanvasEvent, ObjectRemovedFromCanvasEvent } from './shared';
import { NewObjectAddedToCanvasEvent, ObjectRemovedFromCanvasEvent, ObjectsReorderedOnCanvasEvent } from './shared';
import { useEditableElement } from './useEditableElement';
export interface DashboardEditPaneState extends SceneObjectState {
@@ -60,6 +60,14 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
this.clearSelection();
})
);
this._subs.add(
dashboard.subscribeToEvent(ObjectsReorderedOnCanvasEvent, ({ payload }) => {
if (this.state.tab === 'outline') {
this.forceRender();
}
})
);
}
public enableSelection() {
@@ -62,3 +62,7 @@ export class NewObjectAddedToCanvasEvent extends BusEventWithPayload<SceneObject
export class ObjectRemovedFromCanvasEvent extends BusEventWithPayload<SceneObject> {
static type = 'object-removed-from-canvas';
}
export class ObjectsReorderedOnCanvasEvent extends BusEventWithPayload<SceneObject> {
static type = 'objects-reordered-on-canvas';
}
@@ -15,7 +15,11 @@ import { GRID_COLUMN_COUNT } from 'app/core/constants';
import { t } from 'app/core/internationalization';
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
import { NewObjectAddedToCanvasEvent, ObjectRemovedFromCanvasEvent } from '../../edit-pane/shared';
import {
NewObjectAddedToCanvasEvent,
ObjectRemovedFromCanvasEvent,
ObjectsReorderedOnCanvasEvent,
} from '../../edit-pane/shared';
import { isClonedKey, joinCloneKeys } from '../../utils/clone';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import {
@@ -61,6 +65,22 @@ export class DefaultGridLayoutManager
public readonly descriptor = DefaultGridLayoutManager.descriptor;
public constructor(state: DefaultGridLayoutManagerState) {
super(state);
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
this._subs.add(
this.state.grid.subscribeToState(({ children: newChildren }, { children: prevChildren }) => {
if (newChildren.length === prevChildren.length) {
this.publishEvent(new ObjectsReorderedOnCanvasEvent(this.state.grid), true);
}
})
);
}
public addPanel(vizPanel: VizPanel) {
const panelId = dashboardSceneGraph.getNextPanelId(this);
@@ -349,6 +369,8 @@ export class DefaultGridLayoutManager
children.splice(indexOfRow, 0, ...rowChildren);
}
this.publishEvent(new ObjectRemovedFromCanvasEvent(row), true);
sceneGridLayout.setState({ children });
}
@@ -1,7 +1,11 @@
import { SceneGridItemLike, SceneGridRow, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { NewObjectAddedToCanvasEvent, ObjectRemovedFromCanvasEvent } from '../../edit-pane/shared';
import {
NewObjectAddedToCanvasEvent,
ObjectRemovedFromCanvasEvent,
ObjectsReorderedOnCanvasEvent,
} from '../../edit-pane/shared';
import { isClonedKey } from '../../utils/clone';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
@@ -150,6 +154,7 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
rows.splice(originalIndex, 1);
rows.splice(moveToIndex, 0, row);
this.setState({ rows });
this.publishEvent(new ObjectsReorderedOnCanvasEvent(this), true);
}
public moveRowDown(row: RowItem) {
@@ -171,6 +176,7 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
rows.splice(originalIndex, 1);
this.setState({ rows });
this.publishEvent(new ObjectsReorderedOnCanvasEvent(this), true);
}
public isFirstRow(row: RowItem): boolean {
@@ -20,12 +20,14 @@ export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
return (
<Tab
truncate
className={cx(
isSelected && 'dashboard-selected-element',
isSelectable && !isSelected && 'dashboard-selectable-element'
)}
active={isActive}
role="presentation"
title={titleInterpolated}
href={href}
aria-selected={isActive}
onPointerDown={onSelect}
@@ -7,7 +7,11 @@ import {
} from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { ObjectRemovedFromCanvasEvent } from '../../edit-pane/shared';
import {
NewObjectAddedToCanvasEvent,
ObjectRemovedFromCanvasEvent,
ObjectsReorderedOnCanvasEvent,
} from '../../edit-pane/shared';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
@@ -134,6 +138,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
const tabs = this.state.tabs.slice();
tabs.splice(tabs.indexOf(tab), 0, newTab);
this.setState({ tabs, currentTabIndex: this.state.currentTabIndex });
this.publishEvent(new NewObjectAddedToCanvasEvent(newTab), true);
}
public addTabAfter(tab: TabItem) {
@@ -141,6 +146,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
const tabs = this.state.tabs.slice();
tabs.splice(tabs.indexOf(tab) + 1, 0, newTab);
this.setState({ tabs, currentTabIndex: this.state.currentTabIndex + 1 });
this.publishEvent(new NewObjectAddedToCanvasEvent(newTab), true);
}
public moveTabLeft(tab: TabItem) {
@@ -152,6 +158,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
tabs.splice(currentIndex, 1);
tabs.splice(currentIndex - 1, 0, tab);
this.setState({ tabs, currentTabIndex: this.state.currentTabIndex - 1 });
this.publishEvent(new ObjectsReorderedOnCanvasEvent(this), true);
}
public moveTabRight(tab: TabItem) {
@@ -163,6 +170,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
tabs.splice(currentIndex, 1);
tabs.splice(currentIndex + 1, 0, tab);
this.setState({ tabs, currentTabIndex: this.state.currentTabIndex + 1 });
this.publishEvent(new ObjectsReorderedOnCanvasEvent(this), true);
}
public isFirstTab(tab: TabItem): boolean {
+22 -10
View File
@@ -1189,6 +1189,28 @@
}
},
"edit-pane": {
"add": {
"lib-panel": {
"heading": "Library panel",
"title": "Library panels allow you share and reuse panels between dashboards"
},
"panel": {
"heading": "Panel",
"title": "A container for visualizations and other content"
},
"paste-panel": {
"heading": "Paste panel",
"title": "Paste a panel from the clipboard"
},
"row": {
"heading": "Row",
"title": "A group of panels with an optional header"
},
"tab": {
"heading": "Tab",
"title": "Break up your dashboard into different horizontal tabs"
}
},
"elements": {
"dashboard": "Dashboard",
"objects": "Objects",
@@ -1379,11 +1401,6 @@
},
"toolbar": {
"add": "Add",
"add-panel": "Panel",
"add-panel-description": "A container for visualizations and other content",
"add-panel-lib": "Import library panel",
"add-row": "Row",
"add-tab": "Tab",
"alert-rules": "Alert rules",
"back-to-dashboard": "Back to dashboard",
"dashboard-settings": {
@@ -1406,18 +1423,14 @@
"label": "Exit edit",
"tooltip": "Exits edit mode and discards unsaved changes"
},
"libray-panel-description": "Libray panels allow you share and reuse panels between dashboards",
"mark-favorite": "Mark as favorite",
"more-save-options": "More save options",
"open-original": "Open original dashboard",
"paste-panel": "Paste panel",
"paste-panel-description": "Paste a panel from the clipboard",
"playlist-next": "Go to next dashboard",
"playlist-previous": "Go to previous dashboard",
"playlist-stop": "Stop playlist",
"public-dashboard": "Public",
"refresh": "Refresh dashboard",
"row-description": "A group of panels with an optional header",
"save": "Save dashboard",
"save-dashboard": {
"label": "Save dashboard",
@@ -1437,7 +1450,6 @@
"share-button": "Share",
"show-hidden-elements": "Show hidden",
"switch-old-dashboard": "Switch to old dashboard page",
"tabs-description": "Break up your dashboard into different horizontal tabs",
"unlink-library-panel": "Unlink library panel",
"unmark-favorite": "Unmark as favorite"
},