mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
7343102d59
commit
592b830fd8
@ -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"],
|
||||
|
@ -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(
|
||||
|
@ -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 &': {
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -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';
|
@ -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',
|
||||
}),
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user