Dynamic Dashboards: Fix some of the issues identified during bugbash (#102279)
This commit is contained in:
@@ -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';
|
||||
}
|
||||
|
||||
+23
-1
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user