Compare commits
7 Commits
sriram/SQL
...
113351-add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b0d003e3e | ||
|
|
48579dc946 | ||
|
|
f56ce2da88 | ||
|
|
3a4540def7 | ||
|
|
af8100d52a | ||
|
|
fa50d21811 | ||
|
|
48fe1ec634 |
@@ -1,5 +1,7 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { addNewPanelFromSidebar } from './utils';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: true,
|
||||
@@ -44,5 +46,90 @@ test.describe(
|
||||
.click();
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible();
|
||||
});
|
||||
|
||||
test('can add a panel from the sidebar on a new dashboard', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({});
|
||||
// check that the sidebar is open on Add section
|
||||
expect(await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.newPanelButton)).toBeVisible();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.newPanelButton).click();
|
||||
// check that new panel has been added
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toBeVisible();
|
||||
addNewPanelFromSidebar(dashboardPage, selectors);
|
||||
// check that another has been added
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('adds a new panel from the sidebar into the layout that was selected last', async ({
|
||||
gotoDashboardPage,
|
||||
selectors,
|
||||
page,
|
||||
}) => {
|
||||
const dashboardPage = await gotoDashboardPage({});
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.newPanelButton).click();
|
||||
|
||||
// group into tab
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click();
|
||||
await page.getByText('Group into tab').click();
|
||||
|
||||
// add new panel from the sidebar
|
||||
addNewPanelFromSidebar(dashboardPage, selectors);
|
||||
|
||||
// check that another panel has been added inside the tab
|
||||
const tab = dashboardPage.getByGrafanaSelector(selectors.components.LayoutContainer('tab New tab'));
|
||||
await expect(tab.getByTestId(selectors.components.Panels.Panel.title('New panel'))).toHaveCount(2);
|
||||
|
||||
// add new tab
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addTab).click();
|
||||
|
||||
// add new panel from the sidebar
|
||||
addNewPanelFromSidebar(dashboardPage, selectors);
|
||||
//check that new panel has been added there
|
||||
const tab2 = dashboardPage.getByGrafanaSelector(selectors.components.LayoutContainer('tab New tab 1'));
|
||||
await expect(tab2.getByTestId(selectors.components.Panels.Panel.title('New panel'))).toHaveCount(1);
|
||||
|
||||
// panel is selected
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldInput('Title'))
|
||||
).toBeVisible();
|
||||
addNewPanelFromSidebar(dashboardPage, selectors);
|
||||
await expect(tab2.getByTestId(selectors.components.Panels.Panel.title('New panel'))).toHaveCount(2);
|
||||
|
||||
// group into row
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click();
|
||||
await page.getByText('Group into row').click();
|
||||
// add into the row
|
||||
addNewPanelFromSidebar(dashboardPage, selectors);
|
||||
const row = dashboardPage.getByGrafanaSelector(selectors.components.LayoutContainer('row New row'));
|
||||
|
||||
// scroll to the bottom of the row to load all panels
|
||||
const scrollContainer = page
|
||||
.getByTestId(selectors.components.DashboardEditPaneSplitter.primaryBody)
|
||||
.locator('> div')
|
||||
.first();
|
||||
await scrollContainer.evaluate((el) => el.scrollTo(0, el.scrollHeight));
|
||||
|
||||
await expect(row.getByTestId(selectors.components.Panels.Panel.title('New panel'))).toHaveCount(3);
|
||||
// add new row and add into it
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow).click();
|
||||
addNewPanelFromSidebar(dashboardPage, selectors);
|
||||
|
||||
const row1 = dashboardPage.getByGrafanaSelector(selectors.components.LayoutContainer('row New row 1'));
|
||||
await expect(row1.getByTestId(selectors.components.Panels.Panel.title('New panel'))).toHaveCount(1);
|
||||
|
||||
// panel is selected
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldInput('Title'))
|
||||
).toBeVisible();
|
||||
addNewPanelFromSidebar(dashboardPage, selectors);
|
||||
|
||||
await scrollContainer.evaluate((el) => el.scrollTo(0, el.scrollHeight));
|
||||
|
||||
// check that the new panel is added next to the last panel selected
|
||||
await expect(row1.getByTestId(selectors.components.Panels.Panel.title('New panel'))).toHaveCount(2);
|
||||
});
|
||||
}
|
||||
);
|
||||
);
|
||||
@@ -249,3 +249,8 @@ export async function switchToAutoGrid(page: Page, dashboardPage: DashboardPage)
|
||||
await confirmModal.click();
|
||||
}
|
||||
}
|
||||
|
||||
export async function addNewPanelFromSidebar(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.addButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.newPanelButton).click();
|
||||
}
|
||||
|
||||
@@ -64,6 +64,9 @@ export const versionedComponents = {
|
||||
dockToggle: {
|
||||
'12.4.0': 'data-testid sidebar-dock-toggle',
|
||||
},
|
||||
newPanelButton: {
|
||||
'12.4.0': 'data-testid sidebar add new panel',
|
||||
},
|
||||
},
|
||||
EditPaneHeader: {
|
||||
deleteButton: {
|
||||
@@ -79,6 +82,9 @@ export const versionedComponents = {
|
||||
'12.1.0': 'data-testid EditPaneHeader duplicate',
|
||||
},
|
||||
},
|
||||
LayoutContainer: {
|
||||
'12.4.0': (identifier: string) => `data-testid Layout container ${identifier}`,
|
||||
},
|
||||
TimePicker: {
|
||||
openButton: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid TimePicker Open Button',
|
||||
|
||||
@@ -190,6 +190,9 @@ export const versionedPages = {
|
||||
outlineButton: {
|
||||
'12.4.0': 'data-testid Dashboard Sidebar outline button',
|
||||
},
|
||||
addButton: {
|
||||
'12.4.0': 'data-testid Dashboard Sidebar new button',
|
||||
}
|
||||
},
|
||||
DashNav: {
|
||||
nav: {
|
||||
|
||||
@@ -46,7 +46,7 @@ export function SidebarComp({ children, contextValue }: Props) {
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<div ref={ref} className={className} style={style}>
|
||||
<div ref={ref} className={className} style={style} data-testid="dashboard-edit-pane-sidebar">
|
||||
{!tabsMode && <SidebarResizer />}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -16,10 +16,11 @@ export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
active?: boolean;
|
||||
tooltip?: string;
|
||||
title: string;
|
||||
isAddButton?: boolean;
|
||||
}
|
||||
|
||||
export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
({ icon, active, onClick, title, tooltip, ...restProps }, ref) => {
|
||||
({ icon, active, onClick, title, tooltip, isAddButton, ...restProps }, ref) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const context = useContext(SidebarContext);
|
||||
|
||||
@@ -31,7 +32,8 @@ export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
styles.button,
|
||||
context.compact && styles.compact,
|
||||
active && styles.active,
|
||||
context.position === 'left' && styles.leftButton
|
||||
context.position === 'left' && styles.leftButton,
|
||||
isAddButton && 'addButton'
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -44,7 +46,7 @@ export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
onClick={onClick}
|
||||
{...restProps}
|
||||
>
|
||||
<div className={styles.iconWrapper}>{renderIcon(icon, context.compact)}</div>
|
||||
<div>{renderIcon(icon, isAddButton)}</div>
|
||||
{!context.compact && <div className={cx(styles.title, active && styles.titleActive)}>{title}</div>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
@@ -54,13 +56,13 @@ export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
|
||||
SidebarButton.displayName = 'SidebarButton';
|
||||
|
||||
function renderIcon(icon: IconName | React.ReactNode, compact?: boolean) {
|
||||
function renderIcon(icon: IconName | React.ReactNode, isAddButton?: boolean) {
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isIconName(icon)) {
|
||||
return <Icon name={icon} size={compact ? `lg` : `lg`} />;
|
||||
return <Icon name={icon} size={isAddButton ? 'xl' : 'lg'} />;
|
||||
}
|
||||
|
||||
return icon;
|
||||
@@ -83,7 +85,14 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
color: theme.colors.text.secondary,
|
||||
background: 'transparent',
|
||||
border: `none`,
|
||||
|
||||
'&.addButton': css({
|
||||
svg: {
|
||||
backgroundColor: theme.colors.primary.main,
|
||||
color: theme.colors.getContrastText(theme.colors.primary.main),
|
||||
borderRadius: theme.shape.radius.sm,
|
||||
padding: 2,
|
||||
},
|
||||
}),
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create(['background-color', 'border-color', 'color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
@@ -145,7 +154,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
width: '100%',
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
iconWrapper: css({}),
|
||||
title: css({
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
color: theme.colors.text.secondary,
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface DashboardEditPaneState extends SceneObjectState {
|
||||
isDocked?: boolean;
|
||||
}
|
||||
|
||||
export type DashboardSidebarPaneName = 'element' | 'outline' | 'filters';
|
||||
export type DashboardSidebarPaneName = 'element' | 'outline' | 'filters' | 'add';
|
||||
|
||||
export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
||||
public constructor() {
|
||||
|
||||
@@ -1,38 +1,46 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useSceneObjectState } from '@grafana/scenes';
|
||||
import { SceneObject, SceneObjectState, useSceneObjectState } from '@grafana/scenes';
|
||||
import { Sidebar } from '@grafana/ui';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { onOpenSnapshotOriginalDashboard } from '../scene/GoToSnapshotOriginButton';
|
||||
import { ManagedDashboardNavBarBadge } from '../scene/ManagedDashboardNavBarBadge';
|
||||
import { RowItem } from '../scene/layout-rows/RowItem';
|
||||
import { TabItem } from '../scene/layout-tabs/TabItem';
|
||||
import { ToolbarActionProps } from '../scene/new-toolbar/types';
|
||||
import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction';
|
||||
import { getDefaultVizPanel, getRowOrTabForSceneObject } from '../utils/utils';
|
||||
|
||||
import { DashboardEditPane } from './DashboardEditPane';
|
||||
import { ShareExportDashboardButton } from './DashboardExportButton';
|
||||
import { DashboardOutline } from './DashboardOutline';
|
||||
import { DashboardSidePaneNew } from './DashboardSidePaneNew';
|
||||
import { ElementEditPane } from './ElementEditPane';
|
||||
|
||||
export interface Props {
|
||||
editPane: DashboardEditPane;
|
||||
dashboard: DashboardScene;
|
||||
isDocked?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Making the EditPane rendering completely standalone (not using editPane.Component) in order to pass custom react props
|
||||
*/
|
||||
export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Props) {
|
||||
export function DashboardEditPaneRenderer({ editPane, dashboard }: Props) {
|
||||
const { selection, openPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
|
||||
const { isEditing, meta, uid } = dashboard.useState();
|
||||
const hasUid = Boolean(uid);
|
||||
const selectedObject = selection?.getFirstObject();
|
||||
const isNewElement = selection?.isNewElement() ?? false;
|
||||
// the layout element that was selected when opening the 'add' pane
|
||||
// used when adding new panel from the sidebar
|
||||
const [selectedLayoutElement, setSelectedLayoutElement] = useState<DashboardScene | SceneObject<SceneObjectState>>(
|
||||
dashboard
|
||||
);
|
||||
|
||||
const editableElement = useMemo(() => {
|
||||
if (selection) {
|
||||
@@ -42,6 +50,25 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro
|
||||
return undefined;
|
||||
}, [selection]);
|
||||
|
||||
const onSetLayoutElement = (obj: SceneObject<SceneObjectState> | undefined) => {
|
||||
if (obj) {
|
||||
setSelectedLayoutElement(getRowOrTabForSceneObject(obj) || dashboard);
|
||||
} else {
|
||||
setSelectedLayoutElement(dashboard);
|
||||
}
|
||||
};
|
||||
|
||||
const onAddNewPanel = () => {
|
||||
if (selectedLayoutElement) {
|
||||
const panel = getDefaultVizPanel();
|
||||
if (selectedLayoutElement instanceof DashboardScene) {
|
||||
dashboard.addPanel(panel);
|
||||
} else if (selectedLayoutElement instanceof RowItem || selectedLayoutElement instanceof TabItem) {
|
||||
selectedLayoutElement.getLayout().addPanel(panel);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{editableElement && (
|
||||
@@ -54,6 +81,11 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro
|
||||
/>
|
||||
</Sidebar.OpenPane>
|
||||
)}
|
||||
{openPane === 'add' && (
|
||||
<Sidebar.OpenPane>
|
||||
<DashboardSidePaneNew onAddPanel={onAddNewPanel} dashboard={dashboard} />
|
||||
</Sidebar.OpenPane>
|
||||
)}
|
||||
{openPane === 'outline' && (
|
||||
<Sidebar.OpenPane>
|
||||
<DashboardOutline editPane={editPane} isEditing={isEditing} />
|
||||
@@ -68,6 +100,18 @@ export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Pro
|
||||
<RedoButton dashboard={dashboard} />
|
||||
</>
|
||||
)}
|
||||
<Sidebar.Button
|
||||
icon="plus"
|
||||
isAddButton
|
||||
onClick={() => {
|
||||
onSetLayoutElement(selectedObject);
|
||||
editPane.openPane('add');
|
||||
}}
|
||||
title={t('dashboard.sidebar.add.title', 'Add')}
|
||||
tooltip={t('dashboard.sidebar.add.tooltip', 'Add new element')}
|
||||
data-testid={selectors.pages.Dashboard.Sidebar.addButton}
|
||||
active={selectedObject === null || openPane === 'add'}
|
||||
/>
|
||||
<Sidebar.Button
|
||||
icon="cog"
|
||||
onClick={() => editPane.selectObject(dashboard, dashboard.state.key!)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useEffect, useLayoutEffect } from 'react';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
@@ -25,9 +26,10 @@ interface Props {
|
||||
isEditing?: boolean;
|
||||
body?: React.ReactNode;
|
||||
controls?: React.ReactNode;
|
||||
isNewEmptyDashboard?: boolean;
|
||||
}
|
||||
|
||||
export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls }: Props) {
|
||||
export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls, isNewEmptyDashboard = false }: Props) {
|
||||
const headerHeight = useChromeHeaderHeight();
|
||||
const { editPane } = dashboard.state;
|
||||
const styles = useStyles2(getStyles, headerHeight ?? 0);
|
||||
@@ -63,10 +65,16 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
||||
}
|
||||
}, [isEditing, editPane]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
if (isNewEmptyDashboard) {
|
||||
editPane.openPane('add');
|
||||
}
|
||||
});
|
||||
|
||||
const { selectionContext, openPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
|
||||
|
||||
const sidebarContext = useSidebar({
|
||||
hasOpenPane: Boolean(openPane),
|
||||
hasOpenPane: Boolean(openPane) || isNewEmptyDashboard,
|
||||
contentMargin: 1,
|
||||
position: 'right',
|
||||
persistanceKey: 'dashboard',
|
||||
@@ -120,7 +128,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
||||
</div>
|
||||
|
||||
<Sidebar contextValue={sidebarContext}>
|
||||
<DashboardEditPaneRenderer editPane={editPane} dashboard={dashboard} isDocked={sidebarContext.isDocked} />
|
||||
<DashboardEditPaneRenderer editPane={editPane} dashboard={dashboard} />
|
||||
</Sidebar>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
import { Sidebar, Text, useStyles2 } from '@grafana/ui';
|
||||
import addPanelImg from 'img/dashboards/add-panel.png';
|
||||
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
export function DashboardSidePaneNew({ onAddPanel, dashboard }: { onAddPanel: () => void; dashboard: SceneObject }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const orchestrator = getDashboardSceneFor(dashboard).state.layoutOrchestrator;
|
||||
|
||||
return (
|
||||
<DragDropContext onDragStart={() => orchestrator?.startDraggingNewPanel()} onDragEnd={() => {}}>
|
||||
<Droppable droppableId="side-drop-id" isDropDisabled>
|
||||
{(dropProvided) => (
|
||||
<div className={styles.sidePanel} ref={dropProvided.innerRef} {...dropProvided.droppableProps}>
|
||||
<Sidebar.PaneHeader title={t('dashboard.add.pane-header', 'Add')} />
|
||||
<div className={styles.sidePanelContainer}>
|
||||
<Text weight="medium">{t('dashboard.add.new-panel.title', 'Panel')}</Text>
|
||||
<Text variant="bodySmall">
|
||||
{t('dashboard.add.new-panel.description', 'Drag or click to add a panel')}
|
||||
</Text>
|
||||
<div className={styles.dragContainer}>
|
||||
<Draggable draggableId="new-panel-drag" index={0}>
|
||||
{(dragProvided, dragSnapshot) => {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
data-testid={selectors.components.Sidebar.newPanelButton}
|
||||
tabIndex={0}
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
className={cx(styles.imageContainer, dragSnapshot.isDragging && styles.dragging)}
|
||||
onClick={onAddPanel}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onAddPanel();
|
||||
}
|
||||
}}
|
||||
aria-label={t('dashboard.add.new-panel.title', 'Panel')}
|
||||
>
|
||||
<img alt="Add panel click area" src={addPanelImg} draggable={false} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Draggable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
sidePanel: css({
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
dragging: css({
|
||||
cursor: 'move',
|
||||
}),
|
||||
sidePanelContainer: css({
|
||||
padding: theme.spacing(2),
|
||||
span: {
|
||||
display: 'block',
|
||||
},
|
||||
}),
|
||||
dragContainer: css({
|
||||
height: 150,
|
||||
}),
|
||||
imageContainer: css({
|
||||
cursor: 'pointer',
|
||||
opacity: 0.8,
|
||||
overflow: 'hidden',
|
||||
padding: theme.spacing(2, 0),
|
||||
borderRadius: theme.shape.radius.sm,
|
||||
width: '100%',
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -11,11 +11,16 @@ import {
|
||||
} from '@grafana/scenes';
|
||||
import { createPointerDistance } from '@grafana/ui';
|
||||
|
||||
import { getDefaultVizPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { RowItem } from './layout-rows/RowItem';
|
||||
import { TabItem } from './layout-tabs/TabItem';
|
||||
import { DashboardDropTarget, isDashboardDropTarget } from './types/DashboardDropTarget';
|
||||
|
||||
interface DashboardLayoutOrchestratorState extends SceneObjectState {
|
||||
draggingGridItem?: SceneObjectRef<SceneGridItemLike>;
|
||||
isDraggingNewPanel?: boolean;
|
||||
}
|
||||
|
||||
export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayoutOrchestratorState> {
|
||||
@@ -59,10 +64,19 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayout
|
||||
this.setState({ draggingGridItem: gridItem.getRef() });
|
||||
}
|
||||
|
||||
public startDraggingNewPanel(): void {
|
||||
document.body.addEventListener('pointermove', this._onPointerMove);
|
||||
document.body.addEventListener('pointerup', this._stopDraggingSync, true);
|
||||
|
||||
this.setState({ isDraggingNewPanel: true });
|
||||
}
|
||||
|
||||
private _stopDraggingSync(_evt: PointerEvent) {
|
||||
const gridItem = this.state.draggingGridItem?.resolve();
|
||||
|
||||
if (this._sourceDropTarget !== this._lastDropTarget) {
|
||||
if (this.state.isDraggingNewPanel) {
|
||||
this._dropNewPanel(_evt);
|
||||
} else if (this._sourceDropTarget !== this._lastDropTarget) {
|
||||
// Wrapped in setTimeout to ensure that any event handlers are called
|
||||
// Useful for allowing react-grid-layout to remove placeholders, etc.
|
||||
setTimeout(() => {
|
||||
@@ -76,12 +90,35 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayout
|
||||
logWarning(warningMessage);
|
||||
}
|
||||
});
|
||||
document.body.removeEventListener('pointermove', this._onPointerMove);
|
||||
document.body.removeEventListener('pointerup', this._stopDraggingSync);
|
||||
|
||||
this.setState({ draggingGridItem: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
private _dropNewPanel(_evt: PointerEvent) {
|
||||
const elementsUnderPoint = document.elementsFromPoint(_evt.clientX, _evt.clientY);
|
||||
|
||||
// if the cursor is in the sidebar, don't add panel
|
||||
const isInSidebar = elementsUnderPoint.some((el) => el.closest('[data-testid="dashboard-edit-pane-sidebar"]') !== null);
|
||||
|
||||
if (isInSidebar) {
|
||||
return;
|
||||
}
|
||||
const panel = getDefaultVizPanel();
|
||||
if (!this._lastDropTarget) {
|
||||
// if no lastDropTarget and not in Sidebar, treat the dashboard itself as the drop target
|
||||
this._getDashboard().addPanel(panel);
|
||||
}
|
||||
|
||||
if (this._lastDropTarget instanceof RowItem || this._lastDropTarget instanceof TabItem) {
|
||||
this._lastDropTarget.getLayout().addPanel(panel);
|
||||
}
|
||||
document.body.removeEventListener('pointermove', this._onPointerMove);
|
||||
document.body.removeEventListener('pointerup', this._stopDraggingSync);
|
||||
document.body.removeEventListener('pointerup', this._stopDraggingSync, true);
|
||||
|
||||
this.setState({ draggingGridItem: undefined });
|
||||
this.setState({ isDraggingNewPanel: false });
|
||||
}
|
||||
|
||||
private _onPointerMove(evt: PointerEvent) {
|
||||
|
||||
@@ -103,6 +103,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||
{!editPanel && (
|
||||
<DashboardEditPaneSplitter
|
||||
dashboard={model}
|
||||
isNewEmptyDashboard={!model.state.uid}
|
||||
isEditing={isEditing}
|
||||
controls={controls && <controls.Component model={controls} />}
|
||||
body={renderBody()}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { getTestIdForLayout } from '../../utils/test-utils';
|
||||
import { useDashboardState } from '../../utils/utils';
|
||||
import { useSoloPanelContext } from '../SoloPanelContext';
|
||||
import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions';
|
||||
@@ -33,6 +35,7 @@ export function AutoGridLayoutRenderer({ model }: SceneComponentProps<AutoGridLa
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid={selectors.components.LayoutContainer(getTestIdForLayout(model))}
|
||||
className={cx(styles.container, fillScreen && styles.containerFillScreen, isEditing && styles.containerEditing)}
|
||||
ref={model.containerRef}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
import { serializeDefaultGridLayout } from '../../serialization/layoutSerializers/DefaultGridLayoutSerializer';
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import { getTestIdForLayout } from '../../utils/test-utils';
|
||||
import {
|
||||
forceRenderChildren,
|
||||
getPanelIdForVizPanel,
|
||||
@@ -657,7 +659,10 @@ function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps<Default
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container, isEditing && styles.containerEditing)}>
|
||||
<div
|
||||
className={cx(styles.container, isEditing && styles.containerEditing)}
|
||||
data-testid={selectors.components.LayoutContainer(getTestIdForLayout(model))}
|
||||
>
|
||||
{model.state.grid.Component && <model.state.grid.Component model={model.state.grid} />}
|
||||
{showCanvasActions && (
|
||||
<div className={styles.actionsWrapper}>
|
||||
|
||||
@@ -23,12 +23,18 @@ import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/co
|
||||
import { DashboardDTO } from 'app/types/dashboard';
|
||||
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { AutoGridLayout } from '../scene/layout-auto-grid/AutoGridLayout';
|
||||
import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
|
||||
import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
|
||||
import { RowItem } from '../scene/layout-rows/RowItem';
|
||||
import { TabItem } from '../scene/layout-tabs/TabItem';
|
||||
import { DashboardLayoutGrid } from '../scene/types/DashboardLayoutGrid';
|
||||
import { transformSaveModelSchemaV2ToScene } from '../serialization/transformSaveModelSchemaV2ToScene';
|
||||
import { transformSceneToSaveModelSchemaV2 } from '../serialization/transformSceneToSaveModelSchemaV2';
|
||||
|
||||
import { getRowOrTabForSceneObject } from './utils';
|
||||
|
||||
export function setupLoadDashboardMock(rsp: DeepPartial<DashboardDTO>, spy?: jest.Mock) {
|
||||
const loadDashboardMock = (spy || jest.fn()).mockResolvedValue(rsp);
|
||||
const loadSnapshotMock = (spy || jest.fn()).mockResolvedValue(rsp);
|
||||
@@ -311,3 +317,12 @@ export function getTestDashboardSceneFromSaveModel(spec?: Partial<DashboardV2Spe
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
// returns e.g. data-testid Layout container row Row title
|
||||
export function getTestIdForLayout(model: AutoGridLayout | DashboardLayoutGrid) {
|
||||
const parentRowOrTab = getRowOrTabForSceneObject(model);
|
||||
const parentType = parentRowOrTab instanceof TabItem ? 'tab' : parentRowOrTab instanceof RowItem ? 'row' : '';
|
||||
const parentTitle =
|
||||
parentRowOrTab instanceof TabItem || parentRowOrTab instanceof RowItem ? parentRowOrTab.state.title : '';
|
||||
return `${parentType} ${parentTitle}`;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel';
|
||||
import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions';
|
||||
import { VizPanelSubHeader } from '../scene/VizPanelSubHeader';
|
||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { RowItem } from '../scene/layout-rows/RowItem';
|
||||
import { TabItem } from '../scene/layout-tabs/TabItem';
|
||||
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
|
||||
import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
|
||||
|
||||
@@ -413,6 +415,15 @@ export function getLayoutManagerFor(sceneObject: SceneObject): DashboardLayoutMa
|
||||
throw new Error('Could not find layout manager for scene object');
|
||||
}
|
||||
|
||||
export function getRowOrTabForSceneObject(sceneObject: SceneObject): SceneObject | null {
|
||||
if (sceneObject instanceof RowItem || sceneObject instanceof TabItem) {
|
||||
return sceneObject;
|
||||
} else if (sceneObject.parent) {
|
||||
return getRowOrTabForSceneObject(sceneObject.parent);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getGridItemKeyForPanelId(panelId: number): string {
|
||||
return `grid-item-${panelId}`;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, useStyles2, Text, Box, Stack, TextLink } from '@grafana/ui';
|
||||
import { Button, useStyles2, Text, Box, Stack, TextLink, Icon } from '@grafana/ui';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
||||
|
||||
@@ -42,105 +42,134 @@ const InternalDashboardEmpty = ({ onAddVisualization, onAddLibraryPanel, onImpor
|
||||
!dashboardLibraryDatasourceUid,
|
||||
})}
|
||||
>
|
||||
<Stack alignItems="stretch" justifyContent="center" gap={4} direction="column">
|
||||
<Box borderRadius="lg" borderColor="strong" borderStyle="dashed" padding={4}>
|
||||
<Stack direction="column" alignItems="center" gap={2}>
|
||||
{config.featureToggles.dashboardNewLayouts ? (
|
||||
<Stack alignItems="stretch" justifyContent="center" gap={4} direction="column">
|
||||
<Box padding={4}>
|
||||
<Box marginBottom={2} paddingX={4} display="flex" justifyContent="center">
|
||||
<Icon name="apps" size="xxl" className={styles.appsIcon} />
|
||||
</Box>
|
||||
<Text element="h1" textAlignment="center" weight="medium">
|
||||
<Trans i18nKey="dashboard.empty.add-visualization-header">
|
||||
Start your new dashboard by adding a visualization
|
||||
</Trans>
|
||||
<Trans i18nKey="dashboard.empty.title">New dashboard</Trans>
|
||||
</Text>
|
||||
<Box marginBottom={2} paddingX={4}>
|
||||
<Box marginTop={3} paddingX={4}>
|
||||
<Text element="p" textAlignment="center" color="secondary">
|
||||
<Trans i18nKey="dashboard.empty.add-visualization-body">
|
||||
Select a data source and then query and visualize your data with charts, stats and tables or
|
||||
create lists, markdowns and other widgets.
|
||||
</Trans>
|
||||
<Trans i18nKey="dashboard.empty.description">Add a panel to visualize your data</Trans>
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
size="lg"
|
||||
icon="plus"
|
||||
data-testid={selectors.pages.AddDashboard.itemButton('Create new panel button')}
|
||||
onClick={onAddVisualization}
|
||||
disabled={!onAddVisualization}
|
||||
>
|
||||
<Trans i18nKey="dashboard.empty.add-visualization-button">Add visualization</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Suggested Dashboards Section */}
|
||||
{config.featureToggles.suggestedDashboards &&
|
||||
config.featureToggles.dashboardLibrary &&
|
||||
dashboardLibraryDatasourceUid && <SuggestedDashboards datasourceUid={dashboardLibraryDatasourceUid} />}
|
||||
|
||||
{/* Basic Provisioned Dashboards Section that don't include community dashboards */}
|
||||
{config.featureToggles.dashboardLibrary &&
|
||||
!config.featureToggles.suggestedDashboards &&
|
||||
dashboardLibraryDatasourceUid && (
|
||||
<BasicProvisionedDashboardsEmptyPage datasourceUid={dashboardLibraryDatasourceUid} />
|
||||
)}
|
||||
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} wrap="wrap" gap={4}>
|
||||
<Box borderRadius="lg" borderColor="strong" borderStyle="dashed" padding={3} flex={1}>
|
||||
<Stack direction="column" alignItems="center" gap={1}>
|
||||
<Text element="h3" textAlignment="center" weight="medium">
|
||||
<Trans i18nKey="dashboard.empty.add-library-panel-header">Import panel</Trans>
|
||||
</Text>
|
||||
<Box marginBottom={2}>
|
||||
<Text element="p" textAlignment="center" color="secondary">
|
||||
<Trans i18nKey="dashboard.empty.add-library-panel-body">
|
||||
Add visualizations that are shared with other dashboards.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
icon="plus"
|
||||
fill="outline"
|
||||
data-testid={selectors.pages.AddDashboard.itemButton('Add a panel from the panel library button')}
|
||||
onClick={onAddLibraryPanel}
|
||||
disabled={!onAddLibraryPanel}
|
||||
>
|
||||
<Trans i18nKey="dashboard.empty.add-library-panel-button">Add library panel</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box borderRadius="lg" borderColor="strong" borderStyle="dashed" padding={3} flex={1}>
|
||||
<Stack direction="column" alignItems="center" gap={1}>
|
||||
<Text element="h3" textAlignment="center" weight="medium">
|
||||
<Trans i18nKey="dashboard.empty.import-a-dashboard-header">Import a dashboard</Trans>
|
||||
</Text>
|
||||
<Box marginBottom={2}>
|
||||
<Text element="p" textAlignment="center" color="secondary">
|
||||
<Trans i18nKey="dashboard.empty.import-a-dashboard-body">
|
||||
Import dashboards from files or{' '}
|
||||
<TextLink external href="https://grafana.com/grafana/dashboards/">
|
||||
grafana.com
|
||||
</TextLink>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
icon="upload"
|
||||
fill="outline"
|
||||
data-testid={selectors.pages.AddDashboard.itemButton('Import dashboard button')}
|
||||
onClick={onImportDashboard}
|
||||
disabled={!onImportDashboard}
|
||||
>
|
||||
<Trans i18nKey="dashboard.empty.import-dashboard-button">Import dashboard</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<DashboardExtensionsComponents dashboardLibraryDatasourceUid={dashboardLibraryDatasourceUid} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack alignItems="stretch" justifyContent="center" gap={4} direction="column">
|
||||
<Box borderRadius="lg" borderColor="strong" borderStyle="dashed" padding={4}>
|
||||
<Stack direction="column" alignItems="center" gap={2}>
|
||||
<Text element="h1" textAlignment="center" weight="medium">
|
||||
<Trans i18nKey="dashboard.empty.add-visualization-header">
|
||||
Start your new dashboard by adding a visualization
|
||||
</Trans>
|
||||
</Text>
|
||||
<Box marginBottom={2} paddingX={4}>
|
||||
<Text element="p" textAlignment="center" color="secondary">
|
||||
<Trans i18nKey="dashboard.empty.add-visualization-body">
|
||||
Select a data source and then query and visualize your data with charts, stats and tables or
|
||||
create lists, markdowns and other widgets.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
size="lg"
|
||||
icon="plus"
|
||||
data-testid={selectors.pages.AddDashboard.itemButton('Create new panel button')}
|
||||
onClick={onAddVisualization}
|
||||
disabled={!onAddVisualization}
|
||||
>
|
||||
<Trans i18nKey="dashboard.empty.add-visualization-button">Add visualization</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<DashboardExtensionsComponents dashboardLibraryDatasourceUid={dashboardLibraryDatasourceUid} />
|
||||
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} wrap="wrap" gap={4}>
|
||||
<Box borderRadius="lg" borderColor="strong" borderStyle="dashed" padding={3} flex={1}>
|
||||
<Stack direction="column" alignItems="center" gap={1}>
|
||||
<Text element="h3" textAlignment="center" weight="medium">
|
||||
<Trans i18nKey="dashboard.empty.add-library-panel-header">Import panel</Trans>
|
||||
</Text>
|
||||
<Box marginBottom={2}>
|
||||
<Text element="p" textAlignment="center" color="secondary">
|
||||
<Trans i18nKey="dashboard.empty.add-library-panel-body">
|
||||
Add visualizations that are shared with other dashboards.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
icon="plus"
|
||||
fill="outline"
|
||||
data-testid={selectors.pages.AddDashboard.itemButton('Add a panel from the panel library button')}
|
||||
onClick={onAddLibraryPanel}
|
||||
disabled={!onAddLibraryPanel}
|
||||
>
|
||||
<Trans i18nKey="dashboard.empty.add-library-panel-button">Add library panel</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box borderRadius="lg" borderColor="strong" borderStyle="dashed" padding={3} flex={1}>
|
||||
<Stack direction="column" alignItems="center" gap={1}>
|
||||
<Text element="h3" textAlignment="center" weight="medium">
|
||||
<Trans i18nKey="dashboard.empty.import-a-dashboard-header">Import a dashboard</Trans>
|
||||
</Text>
|
||||
<Box marginBottom={2}>
|
||||
<Text element="p" textAlignment="center" color="secondary">
|
||||
<Trans i18nKey="dashboard.empty.import-a-dashboard-body">
|
||||
Import dashboards from files or{' '}
|
||||
<TextLink external href="https://grafana.com/grafana/dashboards/">
|
||||
grafana.com
|
||||
</TextLink>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
icon="upload"
|
||||
fill="outline"
|
||||
data-testid={selectors.pages.AddDashboard.itemButton('Import dashboard button')}
|
||||
onClick={onImportDashboard}
|
||||
disabled={!onImportDashboard}
|
||||
>
|
||||
<Trans i18nKey="dashboard.empty.import-dashboard-button">Import dashboard</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DashboardExtensionsComponents = ({
|
||||
dashboardLibraryDatasourceUid,
|
||||
}: {
|
||||
dashboardLibraryDatasourceUid: string | null;
|
||||
}) => (
|
||||
<>
|
||||
{/* Suggested Dashboards Section */}
|
||||
{config.featureToggles.suggestedDashboards &&
|
||||
config.featureToggles.dashboardLibrary &&
|
||||
dashboardLibraryDatasourceUid && <SuggestedDashboards datasourceUid={dashboardLibraryDatasourceUid} />}
|
||||
|
||||
{/* Basic Provisioned Dashboards Section that don't include community dashboards */}
|
||||
{config.featureToggles.dashboardLibrary &&
|
||||
!config.featureToggles.suggestedDashboards &&
|
||||
dashboardLibraryDatasourceUid && (
|
||||
<BasicProvisionedDashboardsEmptyPage datasourceUid={dashboardLibraryDatasourceUid} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
export interface Props {
|
||||
dashboard: DashboardModel | DashboardScene;
|
||||
canCreate: boolean;
|
||||
@@ -190,5 +219,8 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
wrapperMaxWidth: css({
|
||||
maxWidth: '890px',
|
||||
}),
|
||||
appsIcon: css({
|
||||
fill: theme.v1.palette.orange,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
BIN
public/img/dashboards/add-panel.png
Normal file
BIN
public/img/dashboards/add-panel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -5216,6 +5216,13 @@
|
||||
"title-matched_one": "Matched {{count}}/{{totalCount}} options",
|
||||
"title-matched_other": "Matched {{count}}/{{totalCount}} options"
|
||||
},
|
||||
"add": {
|
||||
"pane-header": "Add",
|
||||
"new-panel": {
|
||||
"title": "Panel",
|
||||
"description": "Drag or click to add a panel"
|
||||
}
|
||||
},
|
||||
"outline": {
|
||||
"pane-header": "Content outline",
|
||||
"repeated-item": "Repeat",
|
||||
@@ -5433,6 +5440,10 @@
|
||||
"title": "Options",
|
||||
"tooltip": "Dashboard options"
|
||||
},
|
||||
"add": {
|
||||
"title": "Add",
|
||||
"tooltip": "Add a new element"
|
||||
},
|
||||
"edit-schema": {
|
||||
"title": "Code",
|
||||
"tooltip": "Edit as code"
|
||||
|
||||
Reference in New Issue
Block a user