DashboardScene: Panel edit ux tweaks (#82500)

* Panel edit ux

* Update

* Update

* switch panel plugin bugfix

* Icon change

* Update

* Update

* Fixes
This commit is contained in:
Torkel Ödegaard 2024-02-16 13:04:45 +01:00 committed by GitHub
parent 7343102d59
commit 592b830fd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 295 additions and 216 deletions

View File

@ -956,9 +956,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"]
],
"packages/grafana-ui/src/components/Splitter/Splitter.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

View File

@ -18,9 +18,10 @@ export interface Props {
secondaryPaneStyles?: React.CSSProperties;
/**
* Called when ever the size of the primary pane changes
* @param size (float from 0-1)
* @param flexSize (float from 0-1)
*/
onSizeChange?: (size: number) => void;
onSizeChanged?: (flexSize: number, pixelSize: number) => void;
onResizing?: (flexSize: number, pixelSize: number) => void;
children: [React.ReactNode, React.ReactNode];
}
@ -34,15 +35,16 @@ export function Splitter(props: Props) {
initialSize = 0.5,
primaryPaneStyles,
secondaryPaneStyles,
onSizeChange,
onSizeChanged,
onResizing,
dragPosition = 'middle',
children,
} = props;
const { containerRef, firstPaneRef, minDimProp, splitterProps, secondPaneRef } = useSplitter(
direction,
onSizeChange,
children
onSizeChanged,
onResizing
);
const kids = React.Children.toArray(children);
@ -157,7 +159,11 @@ const propsForDirection = {
},
} as const;
function useSplitter(direction: 'row' | 'column', onSizeChange: Props['onSizeChange'], children: Props['children']) {
function useSplitter(
direction: 'row' | 'column',
onSizeChanged: Props['onSizeChanged'],
onResizing: Props['onResizing']
) {
const handleSize = 16;
const splitterRef = useRef<HTMLDivElement | null>(null);
const firstPaneRef = useRef<HTMLDivElement | null>(null);
@ -230,29 +236,32 @@ function useSplitter(direction: 'row' | 'column', onSizeChange: Props['onSizeCha
const dims = firstPaneMeasurements.current!;
const newSize = clamp(primarySizeRef.current + diff, dims[minDimProp], dims[maxDimProp]);
const newFlex = newSize / (containerSize.current! - handleSize);
firstPaneRef.current!.style.flexGrow = `${newFlex}`;
secondPaneRef.current!.style.flexGrow = `${1 - newFlex}`;
const ariaValueNow = clamp(
((newSize - dims[minDimProp]) / (dims[maxDimProp] - dims[minDimProp])) * 100,
0,
100
);
const ariaValueNow = ariaValue(newSize, dims[minDimProp], dims[maxDimProp] - dims[minDimProp]);
splitterRef.current!.ariaValueNow = `${ariaValueNow}`;
onResizing?.(newFlex, newSize);
}
},
[handleSize, clientAxis, minDimProp, maxDimProp]
[handleSize, clientAxis, minDimProp, maxDimProp, onResizing]
);
const onPointerUp = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
splitterRef.current!.releasePointerCapture(e.pointerId);
dragStart.current = null;
onSizeChange?.(parseFloat(firstPaneRef.current!.style.flexGrow));
if (typeof primarySizeRef.current === 'number') {
onSizeChanged?.(parseFloat(firstPaneRef.current!.style.flexGrow), primarySizeRef.current);
}
},
[onSizeChange]
[onSizeChanged]
);
const pressedKeys = useRef(new Set<string>());
@ -290,19 +299,18 @@ function useSplitter(direction: 'row' | 'column', onSizeChange: Props['onSizeCha
const firstPaneDims = firstPaneMeasurements.current!;
const curSize = firstPaneRef.current!.getBoundingClientRect()[measurementProp];
const newSize = clamp(curSize + sizeChange, firstPaneDims[minDimProp], firstPaneDims[maxDimProp]);
const newFlex = newSize / (containerSize.current! - handleSize);
firstPaneRef.current!.style.flexGrow = `${newFlex}`;
secondPaneRef.current!.style.flexGrow = `${1 - newFlex}`;
const ariaValueNow =
((newSize - firstPaneDims[minDimProp]) / (firstPaneDims[maxDimProp] - firstPaneDims[minDimProp])) * 100;
splitterRef.current!.ariaValueNow = `${clamp(ariaValueNow, 0, 100)}`;
splitterRef.current!.ariaValueNow = ariaValue(newSize, firstPaneDims[minDimProp], firstPaneDims[maxDimProp]);
onResizing?.(newFlex, newSize);
keysLastHandledAt.current = time;
window.requestAnimationFrame(handlePressedKeys);
},
[direction, handleSize, minDimProp, maxDimProp, measurementProp]
[direction, handleSize, minDimProp, maxDimProp, measurementProp, onResizing]
);
const onKeyDown = useCallback(
@ -380,9 +388,12 @@ function useSplitter(direction: 'row' | 'column', onSizeChange: Props['onSizeCha
}
pressedKeys.current.delete(e.key);
onSizeChange?.(parseFloat(firstPaneRef.current!.style.flexGrow));
if (typeof primarySizeRef.current === 'number') {
onSizeChanged?.(parseFloat(firstPaneRef.current!.style.flexGrow), primarySizeRef.current);
}
},
[direction, onSizeChange]
[direction, onSizeChanged]
);
const onDoubleClick = useCallback(() => {
@ -403,9 +414,12 @@ function useSplitter(direction: 'row' | 'column', onSizeChange: Props['onSizeCha
if (pressedKeys.current.size > 0) {
pressedKeys.current.clear();
dragStart.current = null;
onSizeChange?.(parseFloat(firstPaneRef.current!.style.flexGrow));
if (typeof primarySizeRef.current === 'number') {
onSizeChanged?.(parseFloat(firstPaneRef.current!.style.flexGrow), primarySizeRef.current);
}
}
}, [onSizeChange]);
}, [onSizeChanged]);
return {
containerRef,
@ -426,6 +440,10 @@ function useSplitter(direction: 'row' | 'column', onSizeChange: Props['onSizeCha
};
}
function ariaValue(value: number, min: number, max: number) {
return `${clamp(((value - min) / (max - min)) * 100, 0, 100)}`;
}
interface MeasureResult {
minWidth: number;
maxWidth: number;
@ -450,7 +468,7 @@ function measureElement<T extends HTMLElement>(ref: T): MeasureResult {
ref.style.height = savedHeight;
ref.style.flexGrow = savedFlex;
return { minWidth, maxWidth, minHeight, maxHeight } as MeasureResult;
return { minWidth, maxWidth, minHeight, maxHeight };
}
function useResizeObserver(

View File

@ -112,7 +112,7 @@ const getStyles = (theme: GrafanaTheme2) => {
justifyContent: 'flex-end',
paddingLeft: theme.spacing(1),
flexGrow: 1,
gap: theme.spacing(0.5),
gap: theme.spacing(1),
minWidth: 0,
'.body-drawer-open &': {

View File

@ -20,7 +20,9 @@ export interface PanelEditorState extends SceneObjectState {
controls?: SceneObject[];
isDirty?: boolean;
panelId: number;
optionsPane?: PanelOptionsPane;
optionsPane: PanelOptionsPane;
optionsCollapsed?: boolean;
optionsPaneSize: number;
dataPane?: PanelDataPane;
vizManager: VizPanelManager;
}
@ -60,12 +62,14 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
private _initDataPane(pluginId: string) {
const skipDataQuery = config.panels[pluginId].skipDataQuery;
if (!skipDataQuery && !this.state.dataPane) {
this.setState({ dataPane: new PanelDataPane(this.state.vizManager) });
} else if (this.state.dataPane) {
if (skipDataQuery && this.state.dataPane) {
locationService.partial({ tab: null }, true);
this.setState({ dataPane: undefined });
}
if (!skipDataQuery && !this.state.dataPane) {
this.setState({ dataPane: new PanelDataPane(this.state.vizManager) });
}
}
public getUrlKey() {
@ -99,19 +103,55 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
}
}
public toggleOptionsPane(withOpenVizPicker?: boolean) {
if (this.state.optionsPane) {
this.setState({ optionsPane: undefined });
} else {
this.setState({
optionsPane: new PanelOptionsPane({
isVizPickerOpen: withOpenVizPicker,
}),
});
}
public toggleOptionsPane() {
this.setState({ optionsCollapsed: !this.state.optionsCollapsed, optionsPaneSize: OPTIONS_PANE_FLEX_DEFAULT });
}
public onOptionsPaneResizing = (flexSize: number, pixelSize: number) => {
if (flexSize <= 0 && pixelSize <= 0) {
return;
}
const optionsPixelSize = (pixelSize / flexSize) * (1 - flexSize);
if (this.state.optionsCollapsed && optionsPixelSize > OPTIONS_PANE_PIXELS_MIN) {
this.setState({ optionsCollapsed: false });
}
if (!this.state.optionsCollapsed && optionsPixelSize < OPTIONS_PANE_PIXELS_MIN) {
this.setState({ optionsCollapsed: true });
}
};
public onOptionsPaneSizeChanged = (flexSize: number, pixelSize: number) => {
if (flexSize <= 0 && pixelSize <= 0) {
return;
}
const optionsPaneSize = 1 - flexSize;
const isSnappedClosed = this.state.optionsPaneSize === 0;
const fullWidth = pixelSize / flexSize;
const snapWidth = OPTIONS_PANE_PIXELS_SNAP / fullWidth;
if (this.state.optionsCollapsed) {
if (isSnappedClosed) {
this.setState({
optionsPaneSize: Math.max(optionsPaneSize, snapWidth),
optionsCollapsed: false,
});
} else {
this.setState({ optionsPaneSize: 0 });
}
} else if (isSnappedClosed) {
this.setState({ optionsPaneSize: optionsPaneSize });
}
};
}
export const OPTIONS_PANE_PIXELS_MIN = 300;
export const OPTIONS_PANE_PIXELS_SNAP = 400;
export const OPTIONS_PANE_FLEX_DEFAULT = 0.25;
export function buildPanelEditScene(panel: VizPanel): PanelEditor {
const panelClone = panel.clone();
const vizPanelMgr = new VizPanelManager(panelClone);
@ -120,5 +160,6 @@ export function buildPanelEditScene(panel: VizPanel): PanelEditor {
panelId: getPanelIdForVizPanel(panel),
optionsPane: new PanelOptionsPane({}),
vizManager: vizPanelMgr,
optionsPaneSize: OPTIONS_PANE_FLEX_DEFAULT,
});
}

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import React from 'react';
import React, { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
@ -9,53 +9,54 @@ import { NavToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils';
import { PanelEditor } from './PanelEditor';
import { VisualizationButton } from './PanelOptionsPane';
export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>) {
const dashboard = getDashboardSceneFor(model);
const { optionsPane, vizManager, dataPane } = model.useState();
const { optionsPane, vizManager, dataPane, optionsPaneSize } = model.useState();
const { controls } = dashboard.useState();
const styles = useStyles2(getStyles);
const [vizPaneStyles, optionsPaneStyles] = useMemo(() => {
if (optionsPaneSize > 0) {
return [{ flexGrow: 1 - optionsPaneSize }, { minWidth: 'unset', overflow: 'hidden', flexGrow: optionsPaneSize }];
} else {
return [{ flexGrow: 1 }, { minWidth: 'unset', flexGrow: 0 }];
}
}, [optionsPaneSize]);
return (
<>
<NavToolbarActions dashboard={dashboard} />
<div className={styles.canvasContent}>
{controls && (
<div className={styles.controls}>
{controls.map((control) => (
<control.Component key={control.state.key} model={control} />
))}
{!optionsPane && (
<VisualizationButton
pluginId={vizManager.state.panel.state.pluginId}
onOpen={() => model.toggleOptionsPane(true)}
isOpen={false}
onTogglePane={() => model.toggleOptionsPane()}
/>
)}
</div>
)}
<Splitter
direction="row"
dragPosition="end"
initialSize={0.75}
primaryPaneStyles={vizPaneStyles}
secondaryPaneStyles={optionsPaneStyles}
onResizing={model.onOptionsPaneResizing}
onSizeChanged={model.onOptionsPaneSizeChanged}
>
<div className={styles.body}>
<Splitter
direction="row"
dragPosition="end"
initialSize={0.75}
primaryPaneStyles={{ paddingBottom: !dataPane ? 16 : 0 }}
>
<div className={styles.canvasContent}>
{controls && (
<div className={styles.controls}>
{controls.map((control) => (
<control.Component key={control.state.key} model={control} />
))}
</div>
)}
<Splitter
direction="column"
primaryPaneStyles={{ minHeight: 0, paddingRight: !optionsPane ? 16 : 0 }}
primaryPaneStyles={{ minHeight: 0, paddingBottom: !dataPane ? 16 : 0 }}
secondaryPaneStyles={{ minHeight: 0, overflow: 'hidden' }}
dragPosition="start"
>
<vizManager.Component model={vizManager} />
{dataPane && <dataPane.Component model={dataPane} />}
</Splitter>
{optionsPane && <optionsPane.Component model={optionsPane} />}
</Splitter>
</div>
</div>
</div>
{optionsPane && <optionsPane.Component model={optionsPane} />}
</Splitter>
</>
);
}
@ -84,7 +85,7 @@ function getStyles(theme: GrafanaTheme2) {
flexWrap: 'wrap',
alignItems: 'center',
gap: theme.spacing(1),
padding: theme.spacing(2),
padding: theme.spacing(2, 0, 2, 2),
}),
};
}

View File

@ -0,0 +1,88 @@
import React, { useMemo } from 'react';
import { sceneGraph } from '@grafana/scenes';
import { OptionFilter, renderSearchHits } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
import { getFieldOverrideCategories } from 'app/features/dashboard/components/PanelEditor/getFieldOverrideElements';
import { getPanelFrameCategory2 } from 'app/features/dashboard/components/PanelEditor/getPanelFrameOptions';
import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
import { VizPanelManager } from './VizPanelManager';
interface Props {
vizManager: VizPanelManager;
searchQuery: string;
listMode: OptionFilter;
}
export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMode }) => {
const { panel } = vizManager.state;
const { data } = sceneGraph.getData(panel).useState();
const { options, fieldConfig } = panel.useState();
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, fieldConfig]);
const justOverrides = useMemo(
() =>
getFieldOverrideCategories(
fieldConfig,
panel.getPlugin()?.fieldConfigRegistry!,
data?.series ?? [],
searchQuery,
(newConfig) => {
panel.setState({
fieldConfig: newConfig,
});
}
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[searchQuery, panel, fieldConfig]
);
const isSearching = searchQuery.length > 0;
const mainBoxElements: React.ReactNode[] = [];
if (isSearching) {
mainBoxElements.push(
renderSearchHits([panelFrameOptions, ...(visualizationOptions ?? [])], justOverrides, searchQuery)
);
} else {
switch (listMode) {
case OptionFilter.All:
mainBoxElements.push(panelFrameOptions.render());
for (const item of visualizationOptions ?? []) {
mainBoxElements.push(item.render());
}
for (const item of justOverrides) {
mainBoxElements.push(item.render());
}
break;
case OptionFilter.Overrides:
for (const item of justOverrides) {
mainBoxElements.push(item.render());
}
default:
break;
}
}
return mainBoxElements;
});
PanelOptions.displayName = 'PanelOptions';

View File

@ -4,14 +4,12 @@ import React, { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { SceneComponentProps, SceneObjectBase, SceneObjectState, sceneGraph } from '@grafana/scenes';
import { Box, ButtonGroup, FilterInput, RadioButtonGroup, ToolbarButton, useStyles2 } from '@grafana/ui';
import { OptionFilter, renderSearchHits } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
import { getFieldOverrideCategories } from 'app/features/dashboard/components/PanelEditor/getFieldOverrideElements';
import { getPanelFrameCategory2 } from 'app/features/dashboard/components/PanelEditor/getPanelFrameOptions';
import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
import { FilterInput, Stack, ToolbarButton, useStyles2 } from '@grafana/ui';
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
import { getAllPanelPluginMeta } from 'app/features/panel/state/util';
import { PanelEditor } from './PanelEditor';
import { PanelOptions } from './PanelOptions';
import { PanelVizTypePicker } from './PanelVizTypePicker';
export interface PanelOptionsPaneState extends SceneObjectState {
@ -29,10 +27,6 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
});
}
public getVizManager() {
return sceneGraph.getAncestor(this, PanelEditor).state.vizManager;
}
onToggleVizPicker = () => {
this.setState({ isVizPickerOpen: !this.state.isVizPickerOpen });
};
@ -52,114 +46,53 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
const { isVizPickerOpen, searchQuery, listMode } = model.useState();
const panelManager = model.getVizManager();
const { panel } = panelManager.state;
const dataObject = sceneGraph.getData(panel);
const { data } = dataObject.useState();
const { pluginId, options, fieldConfig } = panel.useState();
const editor = sceneGraph.getAncestor(model, PanelEditor);
const { optionsCollapsed, vizManager } = editor.useState();
const { pluginId } = vizManager.state.panel.useState();
const styles = useStyles2(getStyles);
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, fieldConfig]);
const justOverrides = useMemo(
() =>
getFieldOverrideCategories(
fieldConfig,
panel.getPlugin()?.fieldConfigRegistry!,
data?.series ?? [],
searchQuery,
(newConfig) => {
panel.setState({
fieldConfig: newConfig,
});
}
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[searchQuery, panel, fieldConfig]
);
const isSearching = searchQuery.length > 0;
const mainBoxElements: React.ReactNode[] = [];
if (isSearching) {
mainBoxElements.push(
renderSearchHits([panelFrameOptions, ...(visualizationOptions ?? [])], justOverrides, searchQuery)
if (optionsCollapsed) {
return (
<div className={styles.pane}>
<div className={styles.top}>
<ToolbarButton
tooltip={'Open options pane'}
icon={'arrow-to-right'}
onClick={model.onCollapsePane}
variant="canvas"
className={styles.rotateIcon}
data-testid={selectors.components.PanelEditor.toggleVizOptions}
aria-label={'Open options pane'}
/>
</div>
</div>
);
} else {
switch (listMode) {
case OptionFilter.All:
mainBoxElements.push(panelFrameOptions.render());
for (const item of visualizationOptions ?? []) {
mainBoxElements.push(item.render());
}
for (const item of justOverrides) {
mainBoxElements.push(item.render());
}
break;
case OptionFilter.Overrides:
for (const item of justOverrides) {
mainBoxElements.push(item.render());
}
default:
break;
}
}
return (
<div className={styles.box}>
{!isVizPickerOpen && (
<Box paddingX={1} paddingTop={1}>
<VisualizationButton
pluginId={pluginId}
onOpen={model.onToggleVizPicker}
isOpen={isVizPickerOpen}
onTogglePane={model.onCollapsePane}
/>
</Box>
)}
{isVizPickerOpen && (
<PanelVizTypePicker panelManager={panelManager} onChange={model.onToggleVizPicker} data={data} />
)}
<div className={styles.pane}>
{!isVizPickerOpen && (
<>
<div className={styles.top}>
<VisualizationButton
pluginId={pluginId}
onOpen={model.onToggleVizPicker}
isOpen={isVizPickerOpen}
onTogglePane={model.onCollapsePane}
/>
<FilterInput
className={styles.searchOptions}
value={searchQuery}
placeholder="Search options"
onChange={model.onSetSearchQuery}
/>
{!isSearching && (
<RadioButtonGroup
options={[
{ label: 'All', value: OptionFilter.All },
{ label: 'Overrides', value: OptionFilter.Overrides },
]}
value={listMode}
onChange={model.onSetListMode}
fullWidth
></RadioButtonGroup>
)}
</div>
<div className={styles.mainBox}>{mainBoxElements}</div>
<div className={styles.listOfOptions}>
<PanelOptions vizManager={vizManager} searchQuery={searchQuery} listMode={listMode} />
</div>
</>
)}
{isVizPickerOpen && <PanelVizTypePicker vizManager={vizManager} onChange={model.onToggleVizPicker} />}
</div>
);
};
@ -167,30 +100,37 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
function getStyles(theme: GrafanaTheme2) {
return {
box: css({
pane: css({
display: 'flex',
flexDirection: 'column',
flexGrow: '1',
borderLeft: `1px solid ${theme.colors.border.weak}`,
background: theme.colors.background.primary,
overflow: 'hidden',
border: `1px solid ${theme.colors.border.weak}`,
borderBottom: 'none',
borderTopLeftRadius: theme.shape.radius.default,
}),
top: css({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(1),
gap: theme.spacing(1),
padding: theme.spacing(2, 1),
gap: theme.spacing(2),
}),
mainBox: css({
flexGrow: 1,
background: theme.colors.background.primary,
listOfOptions: css({
display: 'flex',
flexDirection: 'column',
flexGrow: '1',
overflow: 'auto',
}),
searchOptions: css({
minHeight: theme.spacing(4),
}),
searchWrapper: css({
padding: theme.spacing(2, 2, 2, 0),
}),
vizField: css({
marginBottom: theme.spacing(1),
}),
rotateIcon: css({
rotate: '180deg',
}),
};
}
@ -206,40 +146,34 @@ export function VisualizationButton({ pluginId, onOpen, isOpen, onTogglePane }:
const pluginMeta = useMemo(() => getAllPanelPluginMeta().filter((p) => p.id === pluginId)[0], [pluginId]);
return (
<div className={styles.wrapper}>
<ButtonGroup>
<ToolbarButton
className={styles.vizButton}
tooltip="Click to change visualization"
imgSrc={pluginMeta.info.logos.small}
// isOpen={isVizPickerOpen}
onClick={onOpen}
data-testid={selectors.components.PanelEditor.toggleVizPicker}
aria-label="Change Visualization"
variant="canvas"
fullWidth
>
{pluginMeta.name}
</ToolbarButton>
<ToolbarButton
tooltip={isOpen ? 'Close options pane' : 'Show options pane'}
icon={isOpen ? 'angle-right' : 'angle-left'}
onClick={onTogglePane}
variant="canvas"
data-testid={selectors.components.PanelEditor.toggleVizOptions}
aria-label={isOpen ? 'Close options pane' : 'Show options pane'}
/>
</ButtonGroup>
</div>
<Stack gap={1}>
<ToolbarButton
className={styles.vizButton}
tooltip="Click to change visualization"
imgSrc={pluginMeta.info.logos.small}
onClick={onOpen}
data-testid={selectors.components.PanelEditor.toggleVizPicker}
aria-label="Change Visualization"
variant="canvas"
isOpen={false}
fullWidth
>
{pluginMeta.name}
</ToolbarButton>
{/* <ToolbarButton
tooltip={'Close options pane'}
icon={'arrow-to-right'}
onClick={onTogglePane}
variant="canvas"
data-testid={selectors.components.PanelEditor.toggleVizOptions}
aria-label={'Close options pane'}
/> */}
</Stack>
);
}
function getVizButtonStyles(theme: GrafanaTheme2) {
return {
wrapper: css({
display: 'flex',
flexDirection: 'column',
}),
vizButton: css({
textAlign: 'left',
}),

View File

@ -16,12 +16,12 @@ import { VizPanelManager } from './VizPanelManager';
export interface Props {
data?: PanelData;
panelManager: VizPanelManager;
vizManager: VizPanelManager;
onChange: () => void;
}
export function PanelVizTypePicker({ panelManager, data, onChange }: Props) {
const { panel } = panelManager.useState();
export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
const { panel } = vizManager.useState();
const styles = useStyles2(getStyles);
const [searchQuery, setSearchQuery] = useState('');
@ -43,7 +43,7 @@ export function PanelVizTypePicker({ panelManager, data, onChange }: Props) {
];
const onVizTypeChange = (options: VizTypeChangeDetails) => {
panelManager.changePluginType(options.pluginId);
vizManager.changePluginType(options.pluginId);
onChange();
};
@ -84,9 +84,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
padding: theme.spacing(1),
padding: theme.spacing(2, 1),
height: '100%',
gap: theme.spacing(1),
gap: theme.spacing(2),
border: `1px solid ${theme.colors.border.weak}`,
borderRight: 'none',
borderBottom: 'none',