mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard: Panel edit and support for different layout items (#96203)
* Dashboard: Panel edit and support for more layout items * It's working * Fix discard issue * remove unused file * Update * Editing for responsive grid items now work * Update * Update * Review fix
This commit is contained in:
parent
0cd19d76ce
commit
b0fe898fa1
@ -267,6 +267,8 @@ describe('PanelEditor', () => {
|
|||||||
// Just adding an extra stateless behavior to verify unlinking does not remvoe it
|
// Just adding an extra stateless behavior to verify unlinking does not remvoe it
|
||||||
const otherBehavior = jest.fn();
|
const otherBehavior = jest.fn();
|
||||||
const panel = new VizPanel({ key: 'panel-1', pluginId: 'text', $behaviors: [libPanelBehavior, otherBehavior] });
|
const panel = new VizPanel({ key: 'panel-1', pluginId: 'text', $behaviors: [libPanelBehavior, otherBehavior] });
|
||||||
|
new DashboardGridItem({ body: panel });
|
||||||
|
|
||||||
const editScene = buildPanelEditScene(panel);
|
const editScene = buildPanelEditScene(panel);
|
||||||
editScene.onConfirmUnlinkLibraryPanel();
|
editScene.onConfirmUnlinkLibraryPanel();
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ import { saveLibPanel } from 'app/features/library-panels/state/api';
|
|||||||
|
|
||||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||||
import { getPanelChanges } from '../saving/getDashboardChanges';
|
import { getPanelChanges } from '../saving/getDashboardChanges';
|
||||||
import { DashboardGridItem, DashboardGridItemState } from '../scene/layout-default/DashboardGridItem';
|
import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types';
|
||||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||||
import {
|
import {
|
||||||
activateSceneObjectAndParentTree,
|
activateSceneObjectAndParentTree,
|
||||||
@ -54,14 +54,22 @@ export interface PanelEditorState extends SceneObjectState {
|
|||||||
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||||
static Component = PanelEditorRenderer;
|
static Component = PanelEditorRenderer;
|
||||||
|
|
||||||
private _originalLayoutElementState!: DashboardGridItemState;
|
private _layoutItemState?: SceneObjectState;
|
||||||
private _layoutElement!: DashboardGridItem;
|
private _layoutItem: DashboardLayoutItem;
|
||||||
private _originalSaveModel!: Panel;
|
private _originalSaveModel!: Panel;
|
||||||
private _changesHaveBeenMade = false;
|
private _changesHaveBeenMade = false;
|
||||||
|
|
||||||
public constructor(state: PanelEditorState) {
|
public constructor(state: PanelEditorState) {
|
||||||
super(state);
|
super(state);
|
||||||
|
|
||||||
|
const panel = this.state.panelRef.resolve();
|
||||||
|
const layoutItem = panel.parent;
|
||||||
|
if (!layoutItem || !isDashboardLayoutItem(layoutItem)) {
|
||||||
|
throw new Error('Panel must have a parent of type DashboardLayoutItem');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._layoutItem = layoutItem;
|
||||||
|
|
||||||
this.setOriginalState(this.state.panelRef);
|
this.setOriginalState(this.state.panelRef);
|
||||||
this.addActivationHandler(this._activationHandler.bind(this));
|
this.addActivationHandler(this._activationHandler.bind(this));
|
||||||
}
|
}
|
||||||
@ -69,14 +77,12 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
private _activationHandler() {
|
private _activationHandler() {
|
||||||
const panel = this.state.panelRef.resolve();
|
const panel = this.state.panelRef.resolve();
|
||||||
const deactivateParents = activateSceneObjectAndParentTree(panel);
|
const deactivateParents = activateSceneObjectAndParentTree(panel);
|
||||||
const layoutElement = panel.parent;
|
|
||||||
|
|
||||||
this.waitForPlugin();
|
this.waitForPlugin();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (layoutElement instanceof DashboardGridItem) {
|
this._layoutItem.editingCompleted?.(this.state.isDirty || this._changesHaveBeenMade);
|
||||||
layoutElement.editingCompleted(this.state.isDirty || this._changesHaveBeenMade);
|
|
||||||
}
|
|
||||||
if (deactivateParents) {
|
if (deactivateParents) {
|
||||||
deactivateParents();
|
deactivateParents();
|
||||||
}
|
}
|
||||||
@ -102,11 +108,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
const panel = panelRef.resolve();
|
const panel = panelRef.resolve();
|
||||||
|
|
||||||
this._originalSaveModel = vizPanelToPanel(panel);
|
this._originalSaveModel = vizPanelToPanel(panel);
|
||||||
|
this._layoutItemState = sceneUtils.cloneSceneObjectState(this._layoutItem.state);
|
||||||
if (panel.parent instanceof DashboardGridItem) {
|
|
||||||
this._originalLayoutElementState = sceneUtils.cloneSceneObjectState(panel.parent.state);
|
|
||||||
this._layoutElement = panel.parent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,9 +136,8 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this._subs.add(panel.subscribeToEvent(SceneObjectStateChangedEvent, handleStateChange));
|
// Subscribe to state changes on the parent (layout item) so we do not miss state changes on the layout item
|
||||||
// Repeat options live on the layout element (DashboardGridItem)
|
this._subs.add(this._layoutItem.subscribeToEvent(SceneObjectStateChangedEvent, handleStateChange));
|
||||||
this._subs.add(this._layoutElement.subscribeToEvent(SceneObjectStateChangedEvent, handleStateChange));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPanel(): VizPanel {
|
public getPanel(): VizPanel {
|
||||||
@ -145,15 +146,12 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
|
|
||||||
private gotPanelPlugin(plugin: PanelPlugin) {
|
private gotPanelPlugin(plugin: PanelPlugin) {
|
||||||
const panel = this.getPanel();
|
const panel = this.getPanel();
|
||||||
const layoutElement = panel.parent;
|
|
||||||
|
|
||||||
// First time initialization
|
// First time initialization
|
||||||
if (this.state.isInitializing) {
|
if (this.state.isInitializing) {
|
||||||
this.setOriginalState(this.state.panelRef);
|
this.setOriginalState(this.state.panelRef);
|
||||||
|
|
||||||
if (layoutElement instanceof DashboardGridItem) {
|
this._layoutItem.editingStarted?.();
|
||||||
layoutElement.editingStarted();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._setupChangeDetection();
|
this._setupChangeDetection();
|
||||||
this._updateDataPane(plugin);
|
this._updateDataPane(plugin);
|
||||||
@ -255,7 +253,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
getDashboardSceneFor(this).removePanel(panel);
|
getDashboardSceneFor(this).removePanel(panel);
|
||||||
} else {
|
} else {
|
||||||
// Revert any layout element changes
|
// Revert any layout element changes
|
||||||
this._layoutElement.setState(this._originalLayoutElementState!);
|
this._layoutItem!.setState(this._layoutItemState!);
|
||||||
}
|
}
|
||||||
|
|
||||||
locationService.partial({ editPanel: null });
|
locationService.partial({ editPanel: null });
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||||
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
|
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
|
||||||
|
|
||||||
import { getPanelFrameCategory2 } from './getPanelFrameOptions';
|
import { getPanelFrameOptions } from './getPanelFrameOptions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panel: VizPanel;
|
panel: VizPanel;
|
||||||
@ -25,13 +25,7 @@ interface Props {
|
|||||||
export const PanelOptions = React.memo<Props>(({ panel, searchQuery, listMode, data }) => {
|
export const PanelOptions = React.memo<Props>(({ panel, searchQuery, listMode, data }) => {
|
||||||
const { options, fieldConfig, _pluginInstanceState } = panel.useState();
|
const { options, fieldConfig, _pluginInstanceState } = panel.useState();
|
||||||
|
|
||||||
const layoutElement = panel.parent!;
|
const panelFrameOptions = useMemo(() => getPanelFrameOptions(panel), [panel]);
|
||||||
const layoutElementState = layoutElement.useState();
|
|
||||||
|
|
||||||
const panelFrameOptions = useMemo(
|
|
||||||
() => getPanelFrameCategory2(panel, layoutElementState),
|
|
||||||
[panel, layoutElementState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const visualizationOptions = useMemo(() => {
|
const visualizationOptions = useMemo(() => {
|
||||||
const plugin = panel.getPlugin();
|
const plugin = panel.getPlugin();
|
||||||
|
@ -1,26 +1,21 @@
|
|||||||
import { SelectableValue } from '@grafana/data';
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { SceneObjectState, SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
|
import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
|
||||||
import { DataLinksInlineEditor, Input, TextArea, Switch, RadioButtonGroup, Select } from '@grafana/ui';
|
import { DataLinksInlineEditor, Input, TextArea, Switch } from '@grafana/ui';
|
||||||
import { GenAIPanelDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton';
|
import { GenAIPanelDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton';
|
||||||
import { GenAIPanelTitleButton } from 'app/features/dashboard/components/GenAI/GenAIPanelTitleButton';
|
import { GenAIPanelTitleButton } from 'app/features/dashboard/components/GenAI/GenAIPanelTitleButton';
|
||||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||||
import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect';
|
|
||||||
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||||
|
|
||||||
import { VizPanelLinks } from '../scene/PanelLinks';
|
import { VizPanelLinks } from '../scene/PanelLinks';
|
||||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
import { isDashboardLayoutItem } from '../scene/types';
|
||||||
import { vizPanelToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
import { vizPanelToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
||||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
export function getPanelFrameCategory2(
|
export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescriptor {
|
||||||
panel: VizPanel,
|
|
||||||
layoutElementState: SceneObjectState
|
|
||||||
): OptionsPaneCategoryDescriptor {
|
|
||||||
const descriptor = new OptionsPaneCategoryDescriptor({
|
const descriptor = new OptionsPaneCategoryDescriptor({
|
||||||
title: 'Panel options',
|
title: 'Panel options',
|
||||||
id: 'Panel options',
|
id: 'Panel options',
|
||||||
@ -30,7 +25,7 @@ export function getPanelFrameCategory2(
|
|||||||
const panelLinksObject = dashboardSceneGraph.getPanelLinks(panel);
|
const panelLinksObject = dashboardSceneGraph.getPanelLinks(panel);
|
||||||
const links = panelLinksObject?.state.rawLinks ?? [];
|
const links = panelLinksObject?.state.rawLinks ?? [];
|
||||||
const dashboard = getDashboardSceneFor(panel);
|
const dashboard = getDashboardSceneFor(panel);
|
||||||
const layoutElement = panel.parent;
|
const layoutElement = panel.parent!;
|
||||||
|
|
||||||
descriptor
|
descriptor
|
||||||
.addItem(
|
.addItem(
|
||||||
@ -97,72 +92,8 @@ export function getPanelFrameCategory2(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (layoutElement instanceof DashboardGridItem) {
|
if (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions) {
|
||||||
const gridItem = layoutElement;
|
descriptor.addCategory(layoutElement.getOptions());
|
||||||
|
|
||||||
const category = new OptionsPaneCategoryDescriptor({
|
|
||||||
title: 'Repeat options',
|
|
||||||
id: 'Repeat options',
|
|
||||||
isOpenDefault: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
category.addItem(
|
|
||||||
new OptionsPaneItemDescriptor({
|
|
||||||
title: 'Repeat by variable',
|
|
||||||
description:
|
|
||||||
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.',
|
|
||||||
render: function renderRepeatOptions() {
|
|
||||||
return (
|
|
||||||
<RepeatRowSelect2
|
|
||||||
id="repeat-by-variable-select"
|
|
||||||
sceneContext={panel}
|
|
||||||
repeat={gridItem.state.variableName}
|
|
||||||
onChange={(value?: string) => gridItem.setRepeatByVariable(value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
category.addItem(
|
|
||||||
new OptionsPaneItemDescriptor({
|
|
||||||
title: 'Repeat direction',
|
|
||||||
showIf: () => Boolean(gridItem.state.variableName),
|
|
||||||
render: function renderRepeatOptions() {
|
|
||||||
const directionOptions: Array<SelectableValue<'h' | 'v'>> = [
|
|
||||||
{ label: 'Horizontal', value: 'h' },
|
|
||||||
{ label: 'Vertical', value: 'v' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RadioButtonGroup
|
|
||||||
options={directionOptions}
|
|
||||||
value={gridItem.state.repeatDirection ?? 'h'}
|
|
||||||
onChange={(value) => gridItem.setState({ repeatDirection: value })}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
category.addItem(
|
|
||||||
new OptionsPaneItemDescriptor({
|
|
||||||
title: 'Max per row',
|
|
||||||
showIf: () => Boolean(gridItem.state.variableName && gridItem.state.repeatDirection === 'h'),
|
|
||||||
render: function renderOption() {
|
|
||||||
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
options={maxPerRowOptions}
|
|
||||||
value={gridItem.state.maxPerRow ?? 4}
|
|
||||||
onChange={(value) => gridItem.setState({ maxPerRow: value.value })}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
descriptor.addCategory(category);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return descriptor;
|
return descriptor;
|
||||||
|
@ -22,10 +22,13 @@ import {
|
|||||||
SceneVariableDependencyConfigLike,
|
SceneVariableDependencyConfigLike,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||||
|
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||||
|
|
||||||
import { getMultiVariableValues, getQueryRunnerFor } from '../../utils/utils';
|
import { getMultiVariableValues, getQueryRunnerFor } from '../../utils/utils';
|
||||||
import { repeatPanelMenuBehavior } from '../PanelMenuBehavior';
|
import { repeatPanelMenuBehavior } from '../PanelMenuBehavior';
|
||||||
import { DashboardRepeatsProcessedEvent } from '../types';
|
import { DashboardLayoutItem, DashboardRepeatsProcessedEvent } from '../types';
|
||||||
|
|
||||||
|
import { getDashboardGridItemOptions } from './DashboardGridItemEditor';
|
||||||
|
|
||||||
export interface DashboardGridItemState extends SceneGridItemStateLike {
|
export interface DashboardGridItemState extends SceneGridItemStateLike {
|
||||||
body: VizPanel;
|
body: VizPanel;
|
||||||
@ -38,9 +41,11 @@ export interface DashboardGridItemState extends SceneGridItemStateLike {
|
|||||||
|
|
||||||
export type RepeatDirection = 'v' | 'h';
|
export type RepeatDirection = 'v' | 'h';
|
||||||
|
|
||||||
export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> implements SceneGridItemLike {
|
export class DashboardGridItem
|
||||||
|
extends SceneObjectBase<DashboardGridItemState>
|
||||||
|
implements SceneGridItemLike, DashboardLayoutItem
|
||||||
|
{
|
||||||
private _prevRepeatValues?: VariableValueSingle[];
|
private _prevRepeatValues?: VariableValueSingle[];
|
||||||
|
|
||||||
protected _variableDependency = new DashboardGridItemVariableDependencyHandler(this);
|
protected _variableDependency = new DashboardGridItemVariableDependencyHandler(this);
|
||||||
|
|
||||||
public constructor(state: DashboardGridItemState) {
|
public constructor(state: DashboardGridItemState) {
|
||||||
@ -189,6 +194,18 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
|
|||||||
this.setState(stateUpdate);
|
this.setState(stateUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DashboardLayoutItem interface start
|
||||||
|
*/
|
||||||
|
public isDashboardLayoutItem: true = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns options for panel edit
|
||||||
|
*/
|
||||||
|
public getOptions(): OptionsPaneCategoryDescriptor {
|
||||||
|
return getDashboardGridItemOptions(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logic to prep panel for panel edit
|
* Logic to prep panel for panel edit
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { RadioButtonGroup, Select } from '@grafana/ui';
|
||||||
|
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||||
|
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||||
|
import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect';
|
||||||
|
|
||||||
|
import { DashboardGridItem } from './DashboardGridItem';
|
||||||
|
|
||||||
|
export function getDashboardGridItemOptions(gridItem: DashboardGridItem) {
|
||||||
|
const category = new OptionsPaneCategoryDescriptor({
|
||||||
|
title: 'Repeat options',
|
||||||
|
id: 'Repeat options',
|
||||||
|
isOpenDefault: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
category.addItem(
|
||||||
|
new OptionsPaneItemDescriptor({
|
||||||
|
title: 'Repeat by variable',
|
||||||
|
description:
|
||||||
|
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.',
|
||||||
|
render: () => <RepeatByOption gridItem={gridItem} />,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
category.addItem(
|
||||||
|
new OptionsPaneItemDescriptor({
|
||||||
|
title: 'Repeat direction',
|
||||||
|
useShowIf: () => {
|
||||||
|
const { variableName } = gridItem.useState();
|
||||||
|
return Boolean(variableName);
|
||||||
|
},
|
||||||
|
render: () => <RepeatDirectionOption gridItem={gridItem} />,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
category.addItem(
|
||||||
|
new OptionsPaneItemDescriptor({
|
||||||
|
title: 'Max per row',
|
||||||
|
useShowIf: () => {
|
||||||
|
const { variableName, repeatDirection } = gridItem.useState();
|
||||||
|
return Boolean(variableName) && repeatDirection === 'h';
|
||||||
|
},
|
||||||
|
render: () => <MaxPerRowOption gridItem={gridItem} />,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionComponentProps {
|
||||||
|
gridItem: DashboardGridItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RepeatDirectionOption({ gridItem }: OptionComponentProps) {
|
||||||
|
const { repeatDirection } = gridItem.useState();
|
||||||
|
|
||||||
|
const directionOptions: Array<SelectableValue<'h' | 'v'>> = [
|
||||||
|
{ label: 'Horizontal', value: 'h' },
|
||||||
|
{ label: 'Vertical', value: 'v' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioButtonGroup
|
||||||
|
options={directionOptions}
|
||||||
|
value={repeatDirection ?? 'h'}
|
||||||
|
onChange={(value) => gridItem.setState({ repeatDirection: value })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MaxPerRowOption({ gridItem }: OptionComponentProps) {
|
||||||
|
const { maxPerRow } = gridItem.useState();
|
||||||
|
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
options={maxPerRowOptions}
|
||||||
|
value={maxPerRow ?? 4}
|
||||||
|
onChange={(value) => gridItem.setState({ maxPerRow: value.value })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RepeatByOption({ gridItem }: OptionComponentProps) {
|
||||||
|
const { variableName } = gridItem.useState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RepeatRowSelect2
|
||||||
|
id="repeat-by-variable-select"
|
||||||
|
sceneContext={gridItem}
|
||||||
|
repeat={variableName}
|
||||||
|
onChange={(value?: string) => gridItem.setRepeatByVariable(value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -370,8 +370,8 @@ export class DefaultGridLayoutManager
|
|||||||
return new DefaultGridLayoutManager({
|
return new DefaultGridLayoutManager({
|
||||||
grid: new SceneGridLayout({
|
grid: new SceneGridLayout({
|
||||||
children: children,
|
children: children,
|
||||||
isDraggable: false,
|
isDraggable: true,
|
||||||
isResizable: false,
|
isResizable: true,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { SceneObjectState, VizPanel, SceneObjectBase, SceneObject, SceneComponentProps } from '@grafana/scenes';
|
import { SceneObjectState, VizPanel, SceneObjectBase, SceneObject, SceneComponentProps } from '@grafana/scenes';
|
||||||
import { Switch } from '@grafana/ui';
|
import { Switch } from '@grafana/ui';
|
||||||
|
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||||
|
|
||||||
|
import { DashboardLayoutItem } from '../types';
|
||||||
|
|
||||||
export interface ResponsiveGridItemState extends SceneObjectState {
|
export interface ResponsiveGridItemState extends SceneObjectState {
|
||||||
body: VizPanel;
|
body: VizPanel;
|
||||||
hideWhenNoData?: boolean;
|
hideWhenNoData?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResponsiveGridItem extends SceneObjectBase<ResponsiveGridItemState> {
|
export class ResponsiveGridItem extends SceneObjectBase<ResponsiveGridItemState> implements DashboardLayoutItem {
|
||||||
public constructor(state: ResponsiveGridItemState) {
|
public constructor(state: ResponsiveGridItemState) {
|
||||||
super(state);
|
super(state);
|
||||||
this.addActivationHandler(() => this._activationHandler());
|
this.addActivationHandler(() => this._activationHandler());
|
||||||
@ -17,8 +20,6 @@ export class ResponsiveGridItem extends SceneObjectBase<ResponsiveGridItemState>
|
|||||||
if (!this.state.hideWhenNoData) {
|
if (!this.state.hideWhenNoData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add hide when no data logic (in a behavior probably)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleHideWhenNoData() {
|
public toggleHideWhenNoData() {
|
||||||
@ -28,20 +29,28 @@ export class ResponsiveGridItem extends SceneObjectBase<ResponsiveGridItemState>
|
|||||||
/**
|
/**
|
||||||
* DashboardLayoutElement interface
|
* DashboardLayoutElement interface
|
||||||
*/
|
*/
|
||||||
public isDashboardLayoutElement: true = true;
|
public isDashboardLayoutItem: true = true;
|
||||||
|
|
||||||
public getOptions?(): OptionsPaneItemDescriptor[] {
|
public getOptions?(): OptionsPaneCategoryDescriptor {
|
||||||
const model = this;
|
const model = this;
|
||||||
|
|
||||||
return [
|
const category = new OptionsPaneCategoryDescriptor({
|
||||||
|
title: 'Layout options',
|
||||||
|
id: 'layout-options',
|
||||||
|
isOpenDefault: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
category.addItem(
|
||||||
new OptionsPaneItemDescriptor({
|
new OptionsPaneItemDescriptor({
|
||||||
title: 'Hide when no data',
|
title: 'Hide when no data',
|
||||||
render: function renderTransparent() {
|
render: function renderTransparent() {
|
||||||
const { hideWhenNoData } = model.state;
|
const { hideWhenNoData } = model.useState();
|
||||||
return <Switch value={hideWhenNoData} id="hide-when-no-data" onChange={() => model.toggleHideWhenNoData()} />;
|
return <Switch value={hideWhenNoData} id="hide-when-no-data" onChange={() => model.toggleHideWhenNoData()} />;
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
];
|
);
|
||||||
|
|
||||||
|
return category;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setBody(body: SceneObject): void {
|
public setBody(body: SceneObject): void {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { BusEventWithPayload, RegistryItem } from '@grafana/data';
|
import { BusEventWithPayload, RegistryItem } from '@grafana/data';
|
||||||
import { SceneObject, VizPanel } from '@grafana/scenes';
|
import { SceneObject, VizPanel } from '@grafana/scenes';
|
||||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A scene object that usually wraps an underlying layout
|
* A scene object that usually wraps an underlying layout
|
||||||
@ -94,17 +94,21 @@ export function isLayoutParent(obj: SceneObject): obj is LayoutParent {
|
|||||||
*/
|
*/
|
||||||
export interface DashboardLayoutItem extends SceneObject {
|
export interface DashboardLayoutItem extends SceneObject {
|
||||||
/**
|
/**
|
||||||
* Marks this object as a layout element
|
* Marks this object as a layout item
|
||||||
*/
|
*/
|
||||||
isDashboardLayoutItem: true;
|
isDashboardLayoutItem: true;
|
||||||
/**
|
/**
|
||||||
* Return layout elements options (like repeat, repeat direction, etc for the default DashboardGridItem)
|
* Return layout item options (like repeat, repeat direction, etc for the default DashboardGridItem)
|
||||||
*/
|
*/
|
||||||
getOptions?(): OptionsPaneItemDescriptor[];
|
getOptions?(): OptionsPaneCategoryDescriptor;
|
||||||
/**
|
/**
|
||||||
* Only implemented by elements that wrap VizPanels
|
* When going into panel edit
|
||||||
|
**/
|
||||||
|
editingStarted?(): void;
|
||||||
|
/**
|
||||||
|
* When coming out of panel edit
|
||||||
*/
|
*/
|
||||||
getVizPanel?(): VizPanel;
|
editingCompleted?(withChanges: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDashboardLayoutItem(obj: SceneObject): obj is DashboardLayoutItem {
|
export function isDashboardLayoutItem(obj: SceneObject): obj is DashboardLayoutItem {
|
||||||
|
@ -11,7 +11,7 @@ import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
|||||||
import { OptionsPaneItemOverrides } from './OptionsPaneItemOverrides';
|
import { OptionsPaneItemOverrides } from './OptionsPaneItemOverrides';
|
||||||
import { OptionPaneItemOverrideInfo } from './types';
|
import { OptionPaneItemOverrideInfo } from './types';
|
||||||
|
|
||||||
export interface OptionsPaneItemProps {
|
export interface OptionsPaneItemInfo {
|
||||||
title: string;
|
title: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
description?: string;
|
description?: string;
|
||||||
@ -19,6 +19,8 @@ export interface OptionsPaneItemProps {
|
|||||||
render: () => React.ReactElement;
|
render: () => React.ReactElement;
|
||||||
skipField?: boolean;
|
skipField?: boolean;
|
||||||
showIf?: () => boolean;
|
showIf?: () => boolean;
|
||||||
|
/** Hook for controlling visibility */
|
||||||
|
useShowIf?: () => boolean;
|
||||||
overrides?: OptionPaneItemOverrideInfo[];
|
overrides?: OptionPaneItemOverrideInfo[];
|
||||||
addon?: ReactNode;
|
addon?: ReactNode;
|
||||||
}
|
}
|
||||||
@ -29,74 +31,87 @@ export interface OptionsPaneItemProps {
|
|||||||
export class OptionsPaneItemDescriptor {
|
export class OptionsPaneItemDescriptor {
|
||||||
parent!: OptionsPaneCategoryDescriptor;
|
parent!: OptionsPaneCategoryDescriptor;
|
||||||
|
|
||||||
constructor(public props: OptionsPaneItemProps) {}
|
constructor(public props: OptionsPaneItemInfo) {}
|
||||||
|
|
||||||
getLabel(searchQuery?: string): ReactNode {
|
|
||||||
const { title, description, overrides, addon } = this.props;
|
|
||||||
|
|
||||||
if (!searchQuery) {
|
|
||||||
// Do not render label for categories with only one child
|
|
||||||
if (this.parent.props.title === title && !overrides?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <OptionPaneLabel title={title} description={description} overrides={overrides} addon={addon} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const categories: React.ReactNode[] = [];
|
|
||||||
|
|
||||||
if (this.parent.parent) {
|
|
||||||
categories.push(this.highlightWord(this.parent.parent.props.title, searchQuery));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.parent.props.title !== title) {
|
|
||||||
categories.push(this.highlightWord(this.parent.props.title, searchQuery));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label description={description && this.highlightWord(description, searchQuery)} category={categories}>
|
|
||||||
{this.highlightWord(title, searchQuery)}
|
|
||||||
{overrides && overrides.length > 0 && <OptionsPaneItemOverrides overrides={overrides} />}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
highlightWord(word: string, query: string) {
|
|
||||||
return (
|
|
||||||
<Highlighter textToHighlight={word} searchWords={[query]} highlightClassName={'search-fragment-highlight'} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderOverrides() {
|
|
||||||
const { overrides } = this.props;
|
|
||||||
if (!overrides || overrides.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render(searchQuery?: string) {
|
render(searchQuery?: string) {
|
||||||
const { title, description, render, showIf, skipField } = this.props;
|
return <OptionsPaneItem key={this.props.title} itemDescriptor={this} searchQuery={searchQuery} />;
|
||||||
const key = `${this.parent.props.id} ${title}`;
|
}
|
||||||
|
|
||||||
if (showIf && !showIf()) {
|
useShowIf() {
|
||||||
|
if (this.props.useShowIf) {
|
||||||
|
return this.props.useShowIf();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.showIf) {
|
||||||
|
return this.props.showIf();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionsPaneItemProps {
|
||||||
|
itemDescriptor: OptionsPaneItemDescriptor;
|
||||||
|
searchQuery?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OptionsPaneItem({ itemDescriptor, searchQuery }: OptionsPaneItemProps) {
|
||||||
|
const { title, description, render, skipField } = itemDescriptor.props;
|
||||||
|
const key = `${itemDescriptor.parent.props.id} ${title}`;
|
||||||
|
const showIf = itemDescriptor.useShowIf();
|
||||||
|
|
||||||
|
if (!showIf) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipField) {
|
||||||
|
return render();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
label={renderOptionLabel(itemDescriptor, searchQuery)}
|
||||||
|
description={description}
|
||||||
|
key={key}
|
||||||
|
aria-label={selectors.components.PanelEditor.OptionsPane.fieldLabel(key)}
|
||||||
|
>
|
||||||
|
{render()}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOptionLabel(itemDescriptor: OptionsPaneItemDescriptor, searchQuery?: string): ReactNode {
|
||||||
|
const { title, description, overrides, addon } = itemDescriptor.props;
|
||||||
|
|
||||||
|
if (!searchQuery) {
|
||||||
|
// Do not render label for categories with only one child
|
||||||
|
if (itemDescriptor.parent.props.title === title && !overrides?.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skipField) {
|
return <OptionPaneLabel title={title} description={description} overrides={overrides} addon={addon} />;
|
||||||
return render();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Field
|
|
||||||
label={this.getLabel(searchQuery)}
|
|
||||||
description={description}
|
|
||||||
key={key}
|
|
||||||
aria-label={selectors.components.PanelEditor.OptionsPane.fieldLabel(key)}
|
|
||||||
>
|
|
||||||
{render()}
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const categories: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
if (itemDescriptor.parent.parent) {
|
||||||
|
categories.push(highlightWord(itemDescriptor.parent.parent.props.title, searchQuery));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemDescriptor.parent.props.title !== title) {
|
||||||
|
categories.push(highlightWord(itemDescriptor.parent.props.title, searchQuery));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label description={description && highlightWord(description, searchQuery)} category={categories}>
|
||||||
|
{highlightWord(title, searchQuery)}
|
||||||
|
{overrides && overrides.length > 0 && <OptionsPaneItemOverrides overrides={overrides} />}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightWord(word: string, query: string) {
|
||||||
|
return <Highlighter textToHighlight={word} searchWords={[query]} highlightClassName={'search-fragment-highlight'} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OptionPanelLabelProps {
|
interface OptionPanelLabelProps {
|
||||||
|
Loading…
Reference in New Issue
Block a user