From b2f2864628bc5ba13c5718411592893d42766f38 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:01:00 +0000 Subject: [PATCH] Scenes: Add panel frame options and visualization options to panel editor (#80884) --- .betterer.results | 11 +- .../panel-edit/PanelEditor.tsx | 6 + .../panel-edit/PanelOptionsPane.tsx | 77 +++++++-- .../panel-edit/PanelVizTypePicker.tsx | 5 + .../panel-edit/VizPanelManager.test.tsx | 2 +- .../version-history/HistorySrv.test.ts | 6 +- .../DashExportModal/DashboardExporter.test.ts | 2 +- .../PanelEditor/OptionsPaneCategory.tsx | 127 ++++++++------- .../OptionsPaneCategoryDescriptor.tsx | 16 +- .../PanelEditor/getPanelFrameOptions.tsx | 148 ++++++++++++++++++ .../PanelEditor/getVisualizationOptions.tsx | 96 ++++++++++++ .../dashboard/utils/dashboard.test.ts | 2 +- 12 files changed, 394 insertions(+), 104 deletions(-) diff --git a/.betterer.results b/.betterer.results index 3a1a3eb80f0..a674fef10e2 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2554,16 +2554,7 @@ exports[`better eslint`] = { ], "public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"], - [0, 0, 0, "Styles should be written using objects.", "9"], - [0, 0, 0, "Styles should be written using objects.", "10"] + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"] ], "public/app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 1b4542ecdc0..640b38f91e9 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -138,6 +138,12 @@ export function buildPanelEditScene(panel: VizPanel): PanelEditor { body: new PanelOptionsPane(vizPanelMgr), width: '100%', }), + primaryPaneStyles: { + minWidth: '0', + }, + secondaryPaneStyles: { + minWidth: '0', + }, }), }); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx index f0c6399545a..1f20a550608 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx @@ -5,6 +5,8 @@ import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { ButtonGroup, FilterInput, RadioButtonGroup, ToolbarButton, useStyles2 } from '@grafana/ui'; +import { getPanelFrameCategory2 } from 'app/features/dashboard/components/PanelEditor/getPanelFrameOptions'; +import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions'; import { getAllPanelPluginMeta } from 'app/features/panel/state/util'; import { PanelVizTypePicker } from './PanelVizTypePicker'; @@ -24,9 +26,27 @@ export class PanelOptionsPane extends SceneObjectBase { static Component = ({ model }: SceneComponentProps) => { const { panelManager } = model; const { panel } = panelManager.state; - const { pluginId } = panel.useState(); + const { pluginId, options } = panel.useState(); const styles = useStyles2(getStyles); const [isVizPickerOpen, setVizPickerOpen] = useState(true); + const panelFrameOptions = useMemo(() => getPanelFrameCategory2(panel), [panel]); + + const visualizationOptions = useMemo(() => { + const plugin = panel.getPlugin(); + if (!plugin) { + return undefined; + } + + return getVisualizationOptions2({ + panel, + plugin: plugin, + eventBus: panel.getPanelContext().eventBus, + instanceState: panel.getPanelContext().instanceState!, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [panel, options]); + + const mainBoxElements = [panelFrameOptions.render(), ...(visualizationOptions?.map((v) => v.render()) ?? [])]; return (
@@ -44,18 +64,23 @@ export class PanelOptionsPane extends SceneObjectBase { )} {!isVizPickerOpen && ( <> - {}} /> - - {/* - Placeholder - */} +
+ {}} + /> + +
+
{mainBoxElements}
)}
@@ -66,20 +91,38 @@ export class PanelOptionsPane extends SceneObjectBase { function getStyles(theme: GrafanaTheme2) { return { + top: css({ + display: 'flex', + flexDirection: 'column', + padding: theme.spacing(1), + gap: theme.spacing(1), + border: `1px solid ${theme.colors.border.weak}`, + borderBottom: 'none', + borderTopLeftRadius: theme.shape.radius.default, + background: theme.colors.background.primary, + }), box: css({ display: 'flex', flexDirection: 'column', flexGrow: '1', - padding: theme.spacing(1), background: theme.colors.background.primary, - border: `1px solid ${theme.colors.border.weak}`, - gap: theme.spacing(1), + overflow: 'hidden', }), wrapper: css({ display: 'flex', flexDirection: 'column', - gap: theme.spacing(2), flexGrow: '1', + gap: theme.spacing(2), + }), + mainBox: css({ + flexGrow: 1, + background: theme.colors.background.primary, + border: `1px solid ${theme.components.panel.borderColor}`, + borderTop: 'none', + overflow: 'auto', + }), + searchOptions: css({ + minHeight: theme.spacing(4), }), }; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx b/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx index 9ae03114a1a..6dd2ad98e78 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx @@ -49,8 +49,13 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'flex', flexDirection: 'column', flexGrow: 1, + padding: theme.spacing(1), height: '100%', gap: theme.spacing(1), + border: `1px solid ${theme.colors.border.weak}`, + borderRight: 'none', + borderBottom: 'none', + borderTopLeftRadius: theme.shape.radius.default, }), filter: css({ minHeight: theme.spacing(4), diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx index 64b5abfc39a..77bf9c31e64 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx @@ -95,7 +95,7 @@ const instance2SettingsMock = { jest.mock('app/core/store', () => ({ exists: jest.fn(), get: jest.fn(), - getObject: jest.fn(), + getObject: jest.fn((_a, b) => b), setObject: jest.fn(), })); diff --git a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts index a35d6ed0180..c2caee31a0e 100644 --- a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts +++ b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts @@ -6,7 +6,11 @@ import { restore, versions } from './__mocks__/dashboardHistoryMocks'; const getMock = jest.fn().mockResolvedValue({}); const postMock = jest.fn().mockResolvedValue({}); -jest.mock('app/core/store'); +jest.mock('app/core/store', () => ({ + get: jest.fn(), + getObject: jest.fn((_a, b) => b), +})); + jest.mock('@grafana/runtime', () => { const original = jest.requireActual('@grafana/runtime'); diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index ca9e3a93a70..1d2771630d2 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -16,7 +16,7 @@ import { DashboardExporter, LibraryElementExport } from './DashboardExporter'; jest.mock('app/core/store', () => { return { getBool: jest.fn(), - getObject: jest.fn(), + getObject: jest.fn((_a, b) => b), get: jest.fn(), }; }); diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx index 9b57326a05f..2b224805c6d 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx @@ -22,7 +22,7 @@ export interface OptionsPaneCategoryProps { sandboxId?: string; } -const CATEGORY_PARAM_NAME = 'showCategory'; +const CATEGORY_PARAM_NAME = 'showCategory' as const; export const OptionsPaneCategory = React.memo( ({ @@ -30,23 +30,21 @@ export const OptionsPaneCategory = React.memo( title, children, forceOpen, - isOpenDefault, + isOpenDefault = true, renderTitle, className, itemsCount, isNested = false, sandboxId, }: OptionsPaneCategoryProps) => { - const initialIsExpanded = isOpenDefault !== false; const [savedState, setSavedState] = useLocalStorage(getOptionGroupStorageKey(id), { - isExpanded: initialIsExpanded, + isExpanded: isOpenDefault, }); - const styles = useStyles2(getStyles); - const [queryParams, updateQueryParams] = useQueryParams(); - const [isExpanded, setIsExpanded] = useState(savedState?.isExpanded ?? initialIsExpanded); + const [isExpanded, setIsExpanded] = useState(savedState?.isExpanded ?? isOpenDefault); const manualClickTime = useRef(0); const ref = useRef(null); + const [queryParams, updateQueryParams] = useQueryParams(); const isOpenFromUrl = queryParams[CATEGORY_PARAM_NAME] === id; useEffect(() => { @@ -92,13 +90,13 @@ export const OptionsPaneCategory = React.memo( }; } + const styles = useStyles2(getStyles); const boxStyles = cx( { [styles.box]: true, [styles.boxNestedExpanded]: isNested && isExpanded, }, - className, - 'options-group' + className ); const headerStyles = cx(styles.header, { @@ -149,61 +147,60 @@ export const OptionsPaneCategory = React.memo( OptionsPaneCategory.displayName = 'OptionsPaneCategory'; -const getStyles = (theme: GrafanaTheme2) => { - return { - box: css` - border-top: 1px solid ${theme.colors.border.weak}; - `, - boxNestedExpanded: css` - margin-bottom: ${theme.spacing(2)}; - `, - title: css` - flex-grow: 1; - overflow: hidden; - line-height: 1.5; - font-size: 1rem; - padding-left: 6px; - font-weight: ${theme.typography.fontWeightMedium}; - margin: 0; - `, - header: css` - display: flex; - cursor: pointer; - align-items: center; - padding: ${theme.spacing(0.5)}; - color: ${theme.colors.text.primary}; - font-weight: ${theme.typography.fontWeightMedium}; +const getStyles = (theme: GrafanaTheme2) => ({ + box: css({ + borderTop: `1px solid ${theme.colors.border.weak}`, + }), + boxNestedExpanded: css({ + marginBottom: theme.spacing(2), + }), + title: css({ + flexGrow: 1, + overflow: 'hidden', + lineHeight: 1.5, + fontSize: '1rem', + paddingLeft: '6px', + fontWeight: theme.typography.fontWeightMedium, + margin: 0, + }), + header: css({ + display: 'flex', + cursor: 'pointer', + alignItems: 'center', + padding: theme.spacing(0.5), + color: theme.colors.text.primary, + fontWeight: theme.typography.fontWeightMedium, - &:hover { - background: ${theme.colors.emphasize(theme.colors.background.primary, 0.03)}; - } - `, - toggleButton: css` - align-self: baseline; - `, - headerExpanded: css` - color: ${theme.colors.text.primary}; - `, - headerNested: css` - padding: ${theme.spacing(0.5, 0, 0.5, 0)}; - `, - body: css` - padding: ${theme.spacing(1, 2, 1, 4)}; - `, - bodyNested: css` - position: relative; - padding-right: 0; - &:before { - content: ''; - position: absolute; - top: 0; - left: 8px; - width: 1px; - height: 100%; - background: ${theme.colors.border.weak}; - } - `, - }; -}; + '&:hover': { + background: theme.colors.emphasize(theme.colors.background.primary, 0.03), + }, + }), + toggleButton: css({ + alignSelf: 'baseline', + }), + headerExpanded: css({ + color: theme.colors.text.primary, + }), + headerNested: css({ + padding: theme.spacing(0.5, 0, 0.5, 0), + }), + body: css({ + padding: theme.spacing(1, 2, 1, 4), + }), + bodyNested: css({ + position: 'relative', + paddingRight: 0, -const getOptionGroupStorageKey = (id: string): string => `${PANEL_EDITOR_UI_STATE_STORAGE_KEY}.optionGroup[${id}]`; + '&:before': { + content: "''", + position: 'absolute', + top: 0, + left: '8px', + width: '1px', + height: '100%', + background: theme.colors.border.weak, + }, + }), +}); + +const getOptionGroupStorageKey = (id: string) => `${PANEL_EDITOR_UI_STATE_STORAGE_KEY}.optionGroup[${id}]`; diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx index 71b5a5e648b..bc65bee8eb1 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx @@ -15,10 +15,10 @@ export interface OptionsPaneCategoryDescriptorProps { customRender?: () => React.ReactNode; sandboxId?: string; } + /** * This is not a real React component but an intermediary to enable deep option search without traversing a React node tree. */ - export class OptionsPaneCategoryDescriptor { items: OptionsPaneItemDescriptor[] = []; categories: OptionsPaneCategoryDescriptor[] = []; @@ -41,14 +41,14 @@ export class OptionsPaneCategoryDescriptor { getCategory(name: string): OptionsPaneCategoryDescriptor { let sub = this.categories.find((c) => c.props.id === name); - if (sub) { - return sub; + if (!sub) { + sub = new OptionsPaneCategoryDescriptor({ + title: name, + id: name, + }); + this.addCategory(sub); } - sub = new OptionsPaneCategoryDescriptor({ - title: name, - id: name, - }); - this.addCategory(sub); + return sub; } diff --git a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx index 56fd78ca84d..b093470a8d8 100644 --- a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { config } from '@grafana/runtime'; +import { VizPanel } from '@grafana/scenes'; import { DataLinksInlineEditor, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui'; import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv'; @@ -171,3 +172,150 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane ) ); } + +export function getPanelFrameCategory2(panel: VizPanel): OptionsPaneCategoryDescriptor { + const descriptor = new OptionsPaneCategoryDescriptor({ + title: 'Panel options', + id: 'Panel options', + isOpenDefault: true, + }); + + return descriptor + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Title', + value: panel.state.title, + popularRank: 1, + render: function renderTitle() { + return ( + panel.setState({ title: e.currentTarget.value })} + /> + ); + }, + // addon: config.featureToggles.dashgpt && , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Description', + description: panel.state.description, + value: panel.state.description, + render: function renderDescription() { + return ( +