mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: Rows layout & editing (#96895)
* 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 * Update * WIP rows * progres * Progress * Progress * Focus selection works * Update * Update * Progress * Update * Editing rows work * Row editing works * fix delete * Update * Row options fix * Fix selecting rows * Update * update * Update * Update * Remove cog icon button * add import to toolbar * Update * Update
This commit is contained in:
parent
d762a96436
commit
71b8f487e0
@ -1965,6 +1965,11 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/edit-pane/VizPanelEditPaneBehavior.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/embedding/EmbeddedDashboardTestPage.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
@ -2088,7 +2093,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "14"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "15"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
@ -2096,6 +2104,11 @@ exports[`better eslint`] = {
|
||||
"public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
|
@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneObjectState, SceneObjectBase, SceneObject, SceneObjectRef } from '@grafana/scenes';
|
||||
import { ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { ElementEditPane } from './ElementEditPane';
|
||||
@ -13,7 +14,17 @@ export interface DashboardEditPaneState extends SceneObjectState {
|
||||
selectedObject?: SceneObjectRef<SceneObject>;
|
||||
}
|
||||
|
||||
export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {}
|
||||
export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
||||
public selectObject(obj: SceneObject) {
|
||||
const currentSelection = this.state.selectedObject?.resolve();
|
||||
if (currentSelection === obj) {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
this.setState({ selectedObject: dashboard.getRef() });
|
||||
} else {
|
||||
this.setState({ selectedObject: obj.getRef() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
editPane: DashboardEditPane;
|
||||
@ -57,13 +68,29 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
|
||||
);
|
||||
}
|
||||
|
||||
const element = getEditableElementFor(selectedObject.resolve());
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} ref={paneRef}>
|
||||
<ElementEditPane obj={selectedObject.resolve()} />
|
||||
<ElementEditPane element={element} key={element.getTypeName()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getEditableElementFor(obj: SceneObject): EditableDashboardElement {
|
||||
if (isEditableDashboardElement(obj)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
for (const behavior of obj.state.$behaviors ?? []) {
|
||||
if (isEditableDashboardElement(behavior)) {
|
||||
return behavior;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Can't find editable element for selected object");
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
wrapper: css({
|
||||
|
@ -1,19 +1,23 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { SceneObjectBase } from '@grafana/scenes';
|
||||
import { Input, TextArea } from '@grafana/ui';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector';
|
||||
import { EditableDashboardElement } from '../scene/types';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
export class DummySelectedObject implements EditableDashboardElement {
|
||||
export class DashboardEditPaneBehavior extends SceneObjectBase implements EditableDashboardElement {
|
||||
public isEditableDashboardElement: true = true;
|
||||
|
||||
constructor(private dashboard: DashboardScene) {}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
const dashboard = this.dashboard;
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
|
||||
// When layout changes we need to update options list
|
||||
const { body } = dashboard.useState();
|
||||
|
||||
const dashboardOptions = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
@ -39,7 +43,9 @@ export class DummySelectedObject implements EditableDashboardElement {
|
||||
);
|
||||
}, [dashboard]);
|
||||
|
||||
return [dashboardOptions];
|
||||
const layoutCategory = useLayoutCategory(body);
|
||||
|
||||
return [dashboardOptions, layoutCategory];
|
||||
}
|
||||
|
||||
public getTypeName(): string {
|
@ -1,21 +1,16 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
import { Stack, useStyles2 } from '@grafana/ui';
|
||||
import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types';
|
||||
|
||||
import { DummySelectedObject } from './DummySelectedObject';
|
||||
import { EditableDashboardElement } from '../scene/types';
|
||||
|
||||
export interface Props {
|
||||
obj: SceneObject;
|
||||
element: EditableDashboardElement;
|
||||
}
|
||||
|
||||
export function ElementEditPane({ obj }: Props) {
|
||||
const element = getEditableElementFor(obj);
|
||||
export function ElementEditPane({ element }: Props) {
|
||||
const categories = element.useEditPaneOptions();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@ -36,25 +31,6 @@ export function ElementEditPane({ obj }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function getEditableElementFor(obj: SceneObject): EditableDashboardElement {
|
||||
if (isEditableDashboardElement(obj)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
for (const behavior of obj.state.$behaviors ?? []) {
|
||||
if (isEditableDashboardElement(behavior)) {
|
||||
return behavior;
|
||||
}
|
||||
}
|
||||
|
||||
// Temp thing to show somethin in edit pane
|
||||
if (obj instanceof DashboardScene) {
|
||||
return new DummySelectedObject(obj);
|
||||
}
|
||||
|
||||
throw new Error("Can't find editable element for selected object");
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
noBorderTop: css({
|
||||
|
@ -0,0 +1,130 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { sceneGraph, SceneObjectBase, VizPanel } from '@grafana/scenes';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
|
||||
|
||||
import {
|
||||
PanelBackgroundSwitch,
|
||||
PanelDescriptionTextArea,
|
||||
PanelFrameTitleInput,
|
||||
} from '../panel-edit/getPanelFrameOptions';
|
||||
import { EditableDashboardElement, isDashboardLayoutItem } from '../scene/types';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
|
||||
export class VizPanelEditPaneBehavior extends SceneObjectBase implements EditableDashboardElement {
|
||||
public isEditableDashboardElement: true = true;
|
||||
|
||||
private getPanel(): VizPanel {
|
||||
const panel = this.parent;
|
||||
|
||||
if (!(panel instanceof VizPanel)) {
|
||||
throw new Error('VizPanelEditPaneBehavior must have a VizPanel parent');
|
||||
}
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
const panel = this.getPanel();
|
||||
const layoutElement = panel.parent!;
|
||||
|
||||
const panelOptions = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
title: 'Panel options',
|
||||
id: 'panel-options',
|
||||
isOpenDefault: true,
|
||||
})
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Title',
|
||||
value: panel.state.title,
|
||||
popularRank: 1,
|
||||
render: function renderTitle() {
|
||||
return <PanelFrameTitleInput panel={panel} />;
|
||||
},
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Description',
|
||||
value: panel.state.description,
|
||||
render: function renderDescription() {
|
||||
return <PanelDescriptionTextArea panel={panel} />;
|
||||
},
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Transparent background',
|
||||
render: function renderTransparent() {
|
||||
return <PanelBackgroundSwitch panel={panel} />;
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [panel]);
|
||||
|
||||
const layoutCategory = useMemo(() => {
|
||||
if (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions) {
|
||||
return layoutElement.getOptions();
|
||||
}
|
||||
return undefined;
|
||||
}, [layoutElement]);
|
||||
|
||||
const { options, fieldConfig, _pluginInstanceState } = panel.useState();
|
||||
const dataProvider = sceneGraph.getData(panel);
|
||||
const { data } = dataProvider.useState();
|
||||
|
||||
const visualizationOptions = useMemo(() => {
|
||||
const plugin = panel.getPlugin();
|
||||
if (!plugin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getVisualizationOptions2({
|
||||
panel,
|
||||
data,
|
||||
plugin: plugin,
|
||||
eventBus: panel.getPanelContext().eventBus,
|
||||
instanceState: _pluginInstanceState,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, panel, options, fieldConfig, _pluginInstanceState]);
|
||||
|
||||
const categories = [panelOptions];
|
||||
if (layoutCategory) {
|
||||
categories.push(layoutCategory);
|
||||
}
|
||||
|
||||
categories.push(...visualizationOptions);
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
public getTypeName(): string {
|
||||
return 'Panel';
|
||||
}
|
||||
|
||||
public onDelete = () => {
|
||||
const layout = dashboardSceneGraph.getLayoutManagerFor(this);
|
||||
layout.removePanel(this.getPanel());
|
||||
};
|
||||
|
||||
public renderActions(): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="secondary">
|
||||
Edit
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary">
|
||||
Copy
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" fill="outline" onClick={this.onDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
value: panel.state.title,
|
||||
popularRank: 1,
|
||||
render: function renderTitle() {
|
||||
return <PanelFrameTitle panel={panel} />;
|
||||
return <PanelFrameTitleInput panel={panel} />;
|
||||
},
|
||||
addon: config.featureToggles.dashgpt && (
|
||||
<GenAIPanelTitleButton
|
||||
@ -50,7 +50,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
title: 'Description',
|
||||
value: panel.state.description,
|
||||
render: function renderDescription() {
|
||||
return <DescriptionTextArea panel={panel} />;
|
||||
return <PanelDescriptionTextArea panel={panel} />;
|
||||
},
|
||||
addon: config.featureToggles.dashgpt && (
|
||||
<GenAIPanelDescriptionButton
|
||||
@ -64,17 +64,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Transparent background',
|
||||
render: function renderTransparent() {
|
||||
return (
|
||||
<Switch
|
||||
value={panel.state.displayMode === 'transparent'}
|
||||
id="transparent-background"
|
||||
onChange={() => {
|
||||
panel.setState({
|
||||
displayMode: panel.state.displayMode === 'transparent' ? 'default' : 'transparent',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <PanelBackgroundSwitch panel={panel} />;
|
||||
},
|
||||
})
|
||||
)
|
||||
@ -116,7 +106,7 @@ function ScenePanelLinksEditor({ panelLinks }: ScenePanelLinksEditorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function PanelFrameTitle({ panel }: { panel: VizPanel }) {
|
||||
export function PanelFrameTitleInput({ panel }: { panel: VizPanel }) {
|
||||
const { title } = panel.useState();
|
||||
|
||||
return (
|
||||
@ -128,7 +118,7 @@ function PanelFrameTitle({ panel }: { panel: VizPanel }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DescriptionTextArea({ panel }: { panel: VizPanel }) {
|
||||
export function PanelDescriptionTextArea({ panel }: { panel: VizPanel }) {
|
||||
const { description } = panel.useState();
|
||||
|
||||
return (
|
||||
@ -140,6 +130,22 @@ function DescriptionTextArea({ panel }: { panel: VizPanel }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function PanelBackgroundSwitch({ panel }: { panel: VizPanel }) {
|
||||
const { displayMode } = panel.useState();
|
||||
|
||||
return (
|
||||
<Switch
|
||||
value={displayMode === 'transparent'}
|
||||
id="transparent-background"
|
||||
onChange={() => {
|
||||
panel.setState({
|
||||
displayMode: panel.state.displayMode === 'transparent' ? 'default' : 'transparent',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function setPanelTitle(panel: VizPanel, title: string) {
|
||||
panel.setState({ title: title, hoverHeader: getUpdatedHoverHeader(title, panel.state.$timeRange) });
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
|
||||
const canSaveAs = contextSrv.hasEditPermissionInFolders;
|
||||
const toolbarActions: ToolbarAction[] = [];
|
||||
const leftActions: ToolbarAction[] = [];
|
||||
const styles = useStyles2(getStyles);
|
||||
const isEditingPanel = Boolean(editPanel);
|
||||
const isViewingPanel = Boolean(viewPanelScene);
|
||||
@ -69,7 +70,8 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
// Means we are not in settings view, fullscreen panel or edit panel
|
||||
const isShowingDashboard = !editview && !isViewingPanel && !isEditingPanel;
|
||||
const isEditingAndShowingDashboard = isEditing && isShowingDashboard;
|
||||
const showScopesSelector = config.featureToggles.singleTopNav && config.featureToggles.scopeFilters;
|
||||
const showScopesSelector = config.featureToggles.singleTopNav && config.featureToggles.scopeFilters && !isEditing;
|
||||
const dashboardNewLayouts = config.featureToggles.dashboardNewLayouts;
|
||||
|
||||
if (!isEditingPanel) {
|
||||
// This adds the precence indicators in enterprise
|
||||
@ -151,74 +153,135 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
addDynamicActions(toolbarActions, dynamicDashNavActions.right, 'icon-actions');
|
||||
}
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'add-panel',
|
||||
condition: isEditingAndShowingDashboard,
|
||||
render: () => (
|
||||
<Dropdown
|
||||
key="add-panel-dropdown"
|
||||
onVisibleChange={(isOpen) => {
|
||||
setIsAddPanelMenuOpen(isOpen);
|
||||
DashboardInteractions.toolbarAddClick();
|
||||
}}
|
||||
overlay={() => (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
key="add-visualization"
|
||||
testId={selectors.pages.AddDashboard.itemButton('Add new visualization menu item')}
|
||||
label={t('dashboard.add-menu.visualization', 'Visualization')}
|
||||
onClick={() => {
|
||||
const vizPanel = dashboard.onCreateNewPanel();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' });
|
||||
dashboard.setState({ editPanel: buildPanelEditScene(vizPanel, true) });
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
key="add-panel-lib"
|
||||
testId={selectors.pages.AddDashboard.itemButton('Add new panel from panel library menu item')}
|
||||
label={t('dashboard.add-menu.import', 'Import from library')}
|
||||
onClick={() => {
|
||||
dashboard.onShowAddLibraryPanelDrawer();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' });
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
key="add-row"
|
||||
testId={selectors.pages.AddDashboard.itemButton('Add new row menu item')}
|
||||
label={t('dashboard.add-menu.row', 'Row')}
|
||||
onClick={() => {
|
||||
dashboard.onCreateNewRow();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' });
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
key="paste-panel"
|
||||
disabled={!hasCopiedPanel}
|
||||
testId={selectors.pages.AddDashboard.itemButton('Add new panel from clipboard menu item')}
|
||||
label={t('dashboard.add-menu.paste-panel', 'Paste panel')}
|
||||
onClick={() => {
|
||||
dashboard.pastePanel();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'paste_panel' });
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
placement="bottom"
|
||||
offset={[0, 6]}
|
||||
>
|
||||
if (dashboardNewLayouts) {
|
||||
leftActions.push({
|
||||
group: 'add-panel',
|
||||
condition: isEditingAndShowingDashboard,
|
||||
render: () => (
|
||||
<Button
|
||||
key="add-panel-button"
|
||||
variant="primary"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
fill="outline"
|
||||
data-testid={selectors.components.PageToolbar.itemButton('Add button')}
|
||||
icon="plus"
|
||||
fill="text"
|
||||
onClick={() => {
|
||||
dashboard.onCreateNewPanel();
|
||||
}}
|
||||
data-testid={selectors.components.PageToolbar.itemButton('add_visualization')}
|
||||
>
|
||||
<Trans i18nKey="dashboard.toolbar.add">Add</Trans>
|
||||
<Icon name={isAddPanelMenuOpen ? 'angle-up' : 'angle-down'} size="lg" />
|
||||
Panel
|
||||
</Button>
|
||||
</Dropdown>
|
||||
),
|
||||
});
|
||||
),
|
||||
});
|
||||
leftActions.push({
|
||||
group: 'add-panel',
|
||||
condition: isEditingAndShowingDashboard,
|
||||
render: () => (
|
||||
<Button
|
||||
key="add-panel-button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon="plus"
|
||||
fill="text"
|
||||
onClick={() => {
|
||||
dashboard.onCreateNewRow();
|
||||
}}
|
||||
data-testid={selectors.components.PageToolbar.itemButton('add_row')}
|
||||
>
|
||||
Row
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
leftActions.push({
|
||||
group: 'add-panel',
|
||||
condition: isEditingAndShowingDashboard,
|
||||
render: () => (
|
||||
<Button
|
||||
key="add-panel-lib"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon="plus"
|
||||
fill="text"
|
||||
data-testid={selectors.pages.AddDashboard.itemButton('Add new panel from panel library menu item')}
|
||||
onClick={() => {
|
||||
dashboard.onShowAddLibraryPanelDrawer();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' });
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
toolbarActions.push({
|
||||
group: 'add-panel',
|
||||
condition: isEditingAndShowingDashboard,
|
||||
render: () => (
|
||||
<Dropdown
|
||||
key="add-panel-dropdown"
|
||||
onVisibleChange={(isOpen) => {
|
||||
setIsAddPanelMenuOpen(isOpen);
|
||||
DashboardInteractions.toolbarAddClick();
|
||||
}}
|
||||
overlay={() => (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
key="add-visualization"
|
||||
testId={selectors.pages.AddDashboard.itemButton('Add new visualization menu item')}
|
||||
label={t('dashboard.add-menu.visualization', 'Visualization')}
|
||||
onClick={() => {
|
||||
const vizPanel = dashboard.onCreateNewPanel();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' });
|
||||
dashboard.setState({ editPanel: buildPanelEditScene(vizPanel, true) });
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
key="add-panel-lib"
|
||||
testId={selectors.pages.AddDashboard.itemButton('Add new panel from panel library menu item')}
|
||||
label={t('dashboard.add-menu.import', 'Import from library')}
|
||||
onClick={() => {
|
||||
dashboard.onShowAddLibraryPanelDrawer();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' });
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
key="add-row"
|
||||
testId={selectors.pages.AddDashboard.itemButton('Add new row menu item')}
|
||||
label={t('dashboard.add-menu.row', 'Row')}
|
||||
onClick={() => {
|
||||
dashboard.onCreateNewRow();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' });
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
key="paste-panel"
|
||||
disabled={!hasCopiedPanel}
|
||||
testId={selectors.pages.AddDashboard.itemButton('Add new panel from clipboard menu item')}
|
||||
label={t('dashboard.add-menu.paste-panel', 'Paste panel')}
|
||||
onClick={() => {
|
||||
dashboard.pastePanel();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'paste_panel' });
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
placement="bottom"
|
||||
offset={[0, 6]}
|
||||
>
|
||||
<Button
|
||||
key="add-panel-button"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
fill="outline"
|
||||
data-testid={selectors.components.PageToolbar.itemButton('Add button')}
|
||||
>
|
||||
<Trans i18nKey="dashboard.toolbar.add">Add</Trans>
|
||||
<Icon name={isAddPanelMenuOpen ? 'angle-up' : 'angle-down'} size="lg" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'playlist-actions',
|
||||
@ -595,6 +658,20 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
},
|
||||
});
|
||||
|
||||
const rigthActionsElements: React.ReactNode[] = renderActionElements(toolbarActions);
|
||||
const leftActionsElements: React.ReactNode[] = renderActionElements(leftActions);
|
||||
const hasActionsToLeftAndRight = showScopesSelector || leftActionsElements.length > 0;
|
||||
|
||||
return (
|
||||
<Stack flex={1} minWidth={0} justifyContent={hasActionsToLeftAndRight ? 'space-between' : 'flex-end'}>
|
||||
{showScopesSelector && <ScopesSelector />}
|
||||
{leftActionsElements.length > 0 && <ToolbarButtonRow alignment="left">{leftActionsElements}</ToolbarButtonRow>}
|
||||
<ToolbarButtonRow alignment="right">{rigthActionsElements}</ToolbarButtonRow>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function renderActionElements(toolbarActions: ToolbarAction[]) {
|
||||
const actionElements: React.ReactNode[] = [];
|
||||
let lastGroup = '';
|
||||
|
||||
@ -610,13 +687,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
actionElements.push(action.render());
|
||||
lastGroup = action.group;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack flex={1} minWidth={0} justifyContent={showScopesSelector ? 'space-between' : 'flex-end'}>
|
||||
{showScopesSelector && <ScopesSelector />}
|
||||
<ToolbarButtonRow alignment="right">{actionElements}</ToolbarButtonRow>
|
||||
</Stack>
|
||||
);
|
||||
return actionElements;
|
||||
}
|
||||
|
||||
function addDynamicActions(
|
||||
|
@ -9,23 +9,18 @@ import {
|
||||
sceneUtils,
|
||||
SceneComponentProps,
|
||||
} from '@grafana/scenes';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardInteractions } from '../../utils/interactions';
|
||||
import {
|
||||
forceRenderChildren,
|
||||
getPanelIdForVizPanel,
|
||||
NEW_PANEL_HEIGHT,
|
||||
NEW_PANEL_WIDTH,
|
||||
getVizPanelKeyForPanelId,
|
||||
getDefaultVizPanel,
|
||||
} from '../../utils/utils';
|
||||
import { RowRepeaterBehavior } from '../RowRepeaterBehavior';
|
||||
import { LayoutEditChrome } from '../layouts-shared/LayoutEditChrome';
|
||||
import { RowActions } from '../row-actions/RowActions';
|
||||
import { DashboardLayoutManager, LayoutEditorProps, LayoutRegistryItem } from '../types';
|
||||
import { DashboardLayoutManager, LayoutRegistryItem } from '../types';
|
||||
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
|
||||
@ -40,6 +35,8 @@ export class DefaultGridLayoutManager
|
||||
extends SceneObjectBase<DefaultGridLayoutManagerState>
|
||||
implements DashboardLayoutManager
|
||||
{
|
||||
public isDashboardLayoutManager: true = true;
|
||||
|
||||
public editModeChanged(isEditing: boolean): void {
|
||||
const updateResizeAndDragging = () => {
|
||||
this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing });
|
||||
@ -387,48 +384,7 @@ export class DefaultGridLayoutManager
|
||||
});
|
||||
}
|
||||
|
||||
public renderEditor() {
|
||||
return <DefaultGridLayoutEditor layoutManager={this} />;
|
||||
}
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<DefaultGridLayoutManager>) => {
|
||||
if (!config.featureToggles.dashboardNewLayouts) {
|
||||
return <model.state.grid.Component model={model.state.grid} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutEditChrome layoutManager={model}>
|
||||
<model.state.grid.Component model={model.state.grid} />
|
||||
</LayoutEditChrome>
|
||||
);
|
||||
return <model.state.grid.Component model={model.state.grid} />;
|
||||
};
|
||||
}
|
||||
|
||||
function DefaultGridLayoutEditor({ layoutManager }: LayoutEditorProps<DefaultGridLayoutManager>) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
fill="outline"
|
||||
icon="plus"
|
||||
onClick={() => {
|
||||
const vizPanel = getDefaultVizPanel();
|
||||
layoutManager.addPanel(vizPanel);
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' });
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="dashboard.add-menu.visualization">Visualization</Trans>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
fill="outline"
|
||||
icon="plus"
|
||||
onClick={() => {
|
||||
layoutManager.addNewRow!();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' });
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="dashboard.add-menu.row">Row</Trans>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneObjectState, VizPanel, SceneObjectBase, SceneObject, SceneComponentProps } from '@grafana/scenes';
|
||||
import { Switch } from '@grafana/ui';
|
||||
import { Switch, useStyles2 } from '@grafana/ui';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
@ -65,7 +68,22 @@ export class ResponsiveGridItem extends SceneObjectBase<ResponsiveGridItemState>
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<ResponsiveGridItem>) => {
|
||||
const { body } = model.useState();
|
||||
const style = useStyles2(getStyles);
|
||||
|
||||
return <body.Component model={body} />;
|
||||
return (
|
||||
<div className={cx(style.wrapper)}>
|
||||
<body.Component model={body} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
wrapper: css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { SceneComponentProps, SceneCSSGridLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
import { Button, Field, Select } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { DashboardInteractions } from '../../utils/interactions';
|
||||
import { getDefaultVizPanel, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||
import { LayoutEditChrome } from '../layouts-shared/LayoutEditChrome';
|
||||
import { DashboardLayoutManager, LayoutRegistryItem, LayoutEditorProps } from '../types';
|
||||
import { getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||
import { DashboardLayoutManager, LayoutRegistryItem } from '../types';
|
||||
|
||||
import { ResponsiveGridItem } from './ResponsiveGridItem';
|
||||
|
||||
@ -18,6 +16,8 @@ export class ResponsiveGridLayoutManager
|
||||
extends SceneObjectBase<ResponsiveGridLayoutManagerState>
|
||||
implements DashboardLayoutManager
|
||||
{
|
||||
public isDashboardLayoutManager: true = true;
|
||||
|
||||
public editModeChanged(isEditing: boolean): void {}
|
||||
|
||||
public addPanel(vizPanel: VizPanel): void {
|
||||
@ -72,8 +72,8 @@ export class ResponsiveGridLayoutManager
|
||||
return panels;
|
||||
}
|
||||
|
||||
public renderEditor() {
|
||||
return <AutomaticGridEditor layoutManager={this} />;
|
||||
public getOptions(): OptionsPaneItemDescriptor[] {
|
||||
return getOptions(this);
|
||||
}
|
||||
|
||||
public getDescriptor(): LayoutRegistryItem {
|
||||
@ -90,7 +90,13 @@ export class ResponsiveGridLayoutManager
|
||||
}
|
||||
|
||||
public static createEmpty() {
|
||||
return new ResponsiveGridLayoutManager({ layout: new SceneCSSGridLayout({ children: [] }) });
|
||||
return new ResponsiveGridLayoutManager({
|
||||
layout: new SceneCSSGridLayout({
|
||||
children: [],
|
||||
templateColumns: 'repeat(auto-fit, minmax(400px, auto))',
|
||||
autoRows: 'minmax(300px, auto)',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public static createFromLayout(layout: DashboardLayoutManager): ResponsiveGridLayoutManager {
|
||||
@ -110,18 +116,22 @@ export class ResponsiveGridLayoutManager
|
||||
});
|
||||
}
|
||||
|
||||
toSaveModel?() {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
activateRepeaters?(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public static Component = ({ model }: SceneComponentProps<ResponsiveGridLayoutManager>) => {
|
||||
return (
|
||||
<LayoutEditChrome layoutManager={model}>
|
||||
<model.state.layout.Component model={model.state.layout} />
|
||||
</LayoutEditChrome>
|
||||
);
|
||||
return <model.state.layout.Component model={model.state.layout} />;
|
||||
};
|
||||
}
|
||||
|
||||
function AutomaticGridEditor({ layoutManager }: LayoutEditorProps<ResponsiveGridLayoutManager>) {
|
||||
function getOptions(layoutManager: ResponsiveGridLayoutManager): OptionsPaneItemDescriptor[] {
|
||||
const options: OptionsPaneItemDescriptor[] = [];
|
||||
|
||||
const cssLayout = layoutManager.state.layout;
|
||||
const { templateColumns, autoRows } = cssLayout.useState();
|
||||
|
||||
const rowOptions: Array<SelectableValue<string>> = [];
|
||||
const sizes = [100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 650];
|
||||
@ -143,39 +153,43 @@ function AutomaticGridEditor({ layoutManager }: LayoutEditorProps<ResponsiveGrid
|
||||
rowOptions.push({ label: `Fixed: ${size}px`, value: `${size}px` });
|
||||
}
|
||||
|
||||
const onColumnsChange = (value: SelectableValue<string>) => {
|
||||
cssLayout.setState({ templateColumns: value.value });
|
||||
};
|
||||
|
||||
const onRowsChange = (value: SelectableValue<string>) => {
|
||||
cssLayout.setState({ autoRows: value.value });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field label="Columns">
|
||||
<Select
|
||||
options={colOptions}
|
||||
value={String(templateColumns)}
|
||||
onChange={onColumnsChange}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Row height">
|
||||
<Select options={rowOptions} value={String(autoRows)} onChange={onRowsChange} />
|
||||
</Field>
|
||||
<Button
|
||||
fill="outline"
|
||||
icon="plus"
|
||||
onClick={() => {
|
||||
const vizPanel = getDefaultVizPanel();
|
||||
layoutManager.addPanel(vizPanel);
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' });
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="dashboard.add-menu.visualization">Visualization</Trans>
|
||||
</Button>
|
||||
</>
|
||||
options.push(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Columns',
|
||||
render: () => {
|
||||
const { templateColumns } = cssLayout.useState();
|
||||
return (
|
||||
<Select
|
||||
options={colOptions}
|
||||
value={String(templateColumns)}
|
||||
onChange={(value) => {
|
||||
cssLayout.setState({ templateColumns: value.value });
|
||||
}}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
options.push(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Rows',
|
||||
render: () => {
|
||||
const { autoRows } = cssLayout.useState();
|
||||
return (
|
||||
<Select
|
||||
options={rowOptions}
|
||||
value={String(autoRows)}
|
||||
onChange={(value) => {
|
||||
cssLayout.setState({ autoRows: value.value });
|
||||
}}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
@ -0,0 +1,242 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useMemo, useRef } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneObjectState, SceneObjectBase, SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { Button, Icon, Input, RadioButtonGroup, Switch, useStyles2 } from '@grafana/ui';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { getDashboardSceneFor, getDefaultVizPanel } from '../../utils/utils';
|
||||
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
|
||||
import { DashboardLayoutManager, EditableDashboardElement, LayoutParent } from '../types';
|
||||
|
||||
import { RowsLayoutManager } from './RowsLayoutManager';
|
||||
|
||||
export interface RowItemState extends SceneObjectState {
|
||||
layout: DashboardLayoutManager;
|
||||
title?: string;
|
||||
isCollapsed?: boolean;
|
||||
isHeaderHidden?: boolean;
|
||||
height?: 'expand' | 'min';
|
||||
}
|
||||
|
||||
export class RowItem extends SceneObjectBase<RowItemState> implements LayoutParent, EditableDashboardElement {
|
||||
public isEditableDashboardElement: true = true;
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
const row = this;
|
||||
|
||||
const rowOptions = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
title: 'Row options',
|
||||
id: 'row-options',
|
||||
isOpenDefault: true,
|
||||
})
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Title',
|
||||
render: () => <RowTitleInput row={row} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Height',
|
||||
render: () => <RowHeightSelect row={row} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Hide row header',
|
||||
render: () => <RowHeaderSwitch row={row} />,
|
||||
})
|
||||
);
|
||||
}, [row]);
|
||||
|
||||
const { layout } = this.useState();
|
||||
const layoutOptions = useLayoutCategory(layout);
|
||||
|
||||
return [rowOptions, layoutOptions];
|
||||
}
|
||||
|
||||
public getTypeName(): string {
|
||||
return 'Row';
|
||||
}
|
||||
|
||||
public onDelete = () => {
|
||||
const layout = sceneGraph.getAncestor(this, RowsLayoutManager);
|
||||
layout.removeRow(this);
|
||||
};
|
||||
|
||||
public renderActions(): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="secondary">
|
||||
Copy
|
||||
</Button>
|
||||
<Button size="sm" variant="primary" onClick={this.onAddPanel} fill="outline">
|
||||
Add panel
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" fill="outline" onClick={this.onDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public getLayout(): DashboardLayoutManager {
|
||||
return this.state.layout;
|
||||
}
|
||||
|
||||
public switchLayout(layout: DashboardLayoutManager): void {
|
||||
this.setState({ layout });
|
||||
}
|
||||
|
||||
public onCollapseToggle = () => {
|
||||
this.setState({ isCollapsed: !this.state.isCollapsed });
|
||||
};
|
||||
|
||||
public onAddPanel = () => {
|
||||
const vizPanel = getDefaultVizPanel();
|
||||
this.state.layout.addPanel(vizPanel);
|
||||
};
|
||||
|
||||
public onEdit = () => {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
dashboard.state.editPane.selectObject(this);
|
||||
};
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<RowItem>) => {
|
||||
const { layout, title, isCollapsed, height = 'expand' } = model.useState();
|
||||
const { isEditing } = getDashboardSceneFor(model).useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text');
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const shouldGrow = !isCollapsed && height === 'expand';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.wrapper, isCollapsed && styles.wrapperCollapsed, shouldGrow && styles.wrapperGrow)}
|
||||
ref={ref}
|
||||
>
|
||||
<div className={styles.rowHeader}>
|
||||
<button
|
||||
onClick={model.onCollapseToggle}
|
||||
className={styles.rowTitleButton}
|
||||
aria-label={isCollapsed ? 'Expand row' : 'Collapse row'}
|
||||
data-testid={selectors.components.DashboardRow.title(titleInterpolated)}
|
||||
>
|
||||
<Icon name={isCollapsed ? 'angle-right' : 'angle-down'} />
|
||||
<span className={styles.rowTitle} role="heading">
|
||||
{titleInterpolated}
|
||||
</span>
|
||||
</button>
|
||||
{isEditing && <Button icon="pen" variant="secondary" size="sm" fill="text" onClick={() => model.onEdit()} />}
|
||||
</div>
|
||||
{!isCollapsed && <layout.Component model={layout} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
rowHeader: css({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
padding: theme.spacing(0, 0, 0.5, 0),
|
||||
margin: theme.spacing(0, 0, 1, 0),
|
||||
alignItems: 'center',
|
||||
|
||||
'&:hover, &:focus-within': {
|
||||
'& > div': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
|
||||
'& > div': {
|
||||
marginBottom: 0,
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
rowTitleButton: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
minWidth: 0,
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
rowTitle: css({
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '100%',
|
||||
flexGrow: 1,
|
||||
minWidth: 0,
|
||||
}),
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
}),
|
||||
wrapperGrow: css({
|
||||
flexGrow: 1,
|
||||
}),
|
||||
wrapperCollapsed: css({
|
||||
flexGrow: 0,
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
rowActions: css({
|
||||
display: 'flex',
|
||||
opacity: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function RowTitleInput({ row }: { row: RowItem }) {
|
||||
const { title } = row.useState();
|
||||
|
||||
return <Input value={title} onChange={(e) => row.setState({ title: e.currentTarget.value })} />;
|
||||
}
|
||||
|
||||
export function RowHeaderSwitch({ row }: { row: RowItem }) {
|
||||
const { isHeaderHidden } = row.useState();
|
||||
|
||||
return (
|
||||
<Switch
|
||||
value={isHeaderHidden}
|
||||
onChange={() => {
|
||||
row.setState({
|
||||
isHeaderHidden: !row.state.isHeaderHidden,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RowHeightSelect({ row }: { row: RowItem }) {
|
||||
const { height = 'expand' } = row.useState();
|
||||
|
||||
const options = [
|
||||
{ label: 'Expand', value: 'expand' as const },
|
||||
{ label: 'Min', value: 'min' as const },
|
||||
];
|
||||
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
options={options}
|
||||
value={height}
|
||||
onChange={(option) =>
|
||||
row.setState({
|
||||
height: option,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { DashboardLayoutManager, LayoutRegistryItem } from '../types';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
|
||||
interface RowsLayoutManagerState extends SceneObjectState {
|
||||
rows: RowItem[];
|
||||
}
|
||||
|
||||
export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> implements DashboardLayoutManager {
|
||||
public isDashboardLayoutManager: true = true;
|
||||
|
||||
public editModeChanged(isEditing: boolean): void {}
|
||||
|
||||
public addPanel(vizPanel: VizPanel): void {}
|
||||
|
||||
public addNewRow(): void {
|
||||
this.setState({
|
||||
rows: [
|
||||
...this.state.rows,
|
||||
new RowItem({
|
||||
title: 'New row',
|
||||
layout: ResponsiveGridLayoutManager.createEmpty(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
public getNextPanelId(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public removePanel(panel: VizPanel) {}
|
||||
|
||||
public removeRow(row: RowItem) {
|
||||
this.setState({
|
||||
rows: this.state.rows.filter((r) => r !== row),
|
||||
});
|
||||
}
|
||||
|
||||
public duplicatePanel(panel: VizPanel): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getVizPanels(): VizPanel[] {
|
||||
const panels: VizPanel[] = [];
|
||||
|
||||
for (const row of this.state.rows) {
|
||||
const innerPanels = row.state.layout.getVizPanels();
|
||||
panels.push(...innerPanels);
|
||||
}
|
||||
|
||||
return panels;
|
||||
}
|
||||
|
||||
public getOptions() {
|
||||
return [];
|
||||
}
|
||||
|
||||
public getDescriptor(): LayoutRegistryItem {
|
||||
return RowsLayoutManager.getDescriptor();
|
||||
}
|
||||
|
||||
public static getDescriptor(): LayoutRegistryItem {
|
||||
return {
|
||||
name: 'Rows',
|
||||
description: 'Rows layout',
|
||||
id: 'rows-layout',
|
||||
createFromLayout: RowsLayoutManager.createFromLayout,
|
||||
};
|
||||
}
|
||||
|
||||
public static createEmpty() {
|
||||
return new RowsLayoutManager({ rows: [] });
|
||||
}
|
||||
|
||||
public static createFromLayout(layout: DashboardLayoutManager): RowsLayoutManager {
|
||||
const row = new RowItem({ layout: layout.clone(), title: 'Row title' });
|
||||
|
||||
return new RowsLayoutManager({ rows: [row] });
|
||||
}
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<RowsLayoutManager>) => {
|
||||
const { rows } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{rows.map((row) => (
|
||||
<RowItem.Component model={row} key={row.state.key!} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}),
|
||||
};
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Select } from '@grafana/ui';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { DashboardLayoutManager, isLayoutParent, LayoutRegistryItem } from '../types';
|
||||
|
||||
import { layoutRegistry } from './layoutRegistry';
|
||||
|
||||
export interface Props {
|
||||
layoutManager: DashboardLayoutManager;
|
||||
}
|
||||
|
||||
export function DashboardLayoutSelector({ layoutManager }: { layoutManager: DashboardLayoutManager }) {
|
||||
const layouts = layoutRegistry.list();
|
||||
const options = layouts.map((layout) => ({
|
||||
label: layout.name,
|
||||
value: layout,
|
||||
}));
|
||||
|
||||
const currentLayoutId = layoutManager.getDescriptor().id;
|
||||
const currentLayoutOption = options.find((option) => option.value.id === currentLayoutId);
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
value={currentLayoutOption}
|
||||
onChange={(option) => changeLayoutTo(layoutManager, option.value!)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLayoutCategory(layoutManager: DashboardLayoutManager) {
|
||||
return useMemo(() => {
|
||||
const layoutCategory = new OptionsPaneCategoryDescriptor({
|
||||
title: 'Layout',
|
||||
id: 'layout-options',
|
||||
isOpenDefault: true,
|
||||
});
|
||||
|
||||
layoutCategory.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Type',
|
||||
render: function renderTitle() {
|
||||
return <DashboardLayoutSelector layoutManager={layoutManager} />;
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (layoutManager.getOptions) {
|
||||
for (const option of layoutManager.getOptions()) {
|
||||
layoutCategory.addItem(option);
|
||||
}
|
||||
}
|
||||
|
||||
return layoutCategory;
|
||||
}, [layoutManager]);
|
||||
}
|
||||
|
||||
function changeLayoutTo(currentLayout: DashboardLayoutManager, newLayoutDescriptor: LayoutRegistryItem) {
|
||||
const layoutParent = currentLayout.parent;
|
||||
if (layoutParent && isLayoutParent(layoutParent)) {
|
||||
layoutParent.switchLayout(newLayoutDescriptor.createFromLayout(currentLayout));
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, Field, Select } from '@grafana/ui';
|
||||
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { DashboardLayoutManager, isLayoutParent, LayoutRegistryItem } from '../types';
|
||||
|
||||
import { layoutRegistry } from './layoutRegistry';
|
||||
|
||||
interface Props {
|
||||
layoutManager: DashboardLayoutManager;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function LayoutEditChrome({ layoutManager, children }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { isEditing } = getDashboardSceneFor(layoutManager).useState();
|
||||
|
||||
const layouts = layoutRegistry.list();
|
||||
const options = layouts.map((layout) => ({
|
||||
label: layout.name,
|
||||
value: layout,
|
||||
}));
|
||||
|
||||
const currentLayoutId = layoutManager.getDescriptor().id;
|
||||
const currentLayoutOption = options.find((option) => option.value.id === currentLayoutId);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{isEditing && (
|
||||
<div className={styles.editHeader}>
|
||||
<Field label="Layout type">
|
||||
<Select
|
||||
options={options}
|
||||
value={currentLayoutOption}
|
||||
onChange={(option) => changeLayoutTo(layoutManager, option.value!)}
|
||||
/>
|
||||
</Field>
|
||||
{layoutManager.renderEditor?.()}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
editHeader: css({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
padding: theme.spacing(0, 1, 0.5, 1),
|
||||
margin: theme.spacing(0, 0, 1, 0),
|
||||
alignItems: 'flex-end',
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
paddingBottom: theme.spacing(1),
|
||||
|
||||
'&:hover, &:focus-within': {
|
||||
'& > div': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
|
||||
'& > div': {
|
||||
marginBottom: 0,
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1 1 0',
|
||||
width: '100%',
|
||||
}),
|
||||
icon: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
rowTitle: css({}),
|
||||
rowActions: css({
|
||||
display: 'flex',
|
||||
opacity: 0,
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: 'opacity 200ms ease-in',
|
||||
},
|
||||
|
||||
'&:hover, &:focus-within': {
|
||||
opacity: 1,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function changeLayoutTo(currentLayout: DashboardLayoutManager, newLayoutDescriptor: LayoutRegistryItem) {
|
||||
const layoutParent = currentLayout.parent;
|
||||
if (layoutParent && isLayoutParent(layoutParent)) {
|
||||
layoutParent.switchLayout(newLayoutDescriptor.createFromLayout(currentLayout));
|
||||
}
|
||||
}
|
@ -2,8 +2,13 @@ import { Registry } from '@grafana/data';
|
||||
|
||||
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types';
|
||||
|
||||
export const layoutRegistry: Registry<LayoutRegistryItem> = new Registry<LayoutRegistryItem>(() => {
|
||||
return [DefaultGridLayoutManager.getDescriptor(), ResponsiveGridLayoutManager.getDescriptor()];
|
||||
return [
|
||||
DefaultGridLayoutManager.getDescriptor(),
|
||||
ResponsiveGridLayoutManager.getDescriptor(),
|
||||
RowsLayoutManager.getDescriptor(),
|
||||
];
|
||||
});
|
||||
|
@ -1,21 +1,20 @@
|
||||
import { BusEventWithPayload, RegistryItem } from '@grafana/data';
|
||||
import { SceneObject, VizPanel } from '@grafana/scenes';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
/**
|
||||
* A scene object that usually wraps an underlying layout
|
||||
* Dealing with all the state management and editing of the layout
|
||||
*/
|
||||
export interface DashboardLayoutManager extends SceneObject {
|
||||
/** Marks it as a DashboardLayoutManager */
|
||||
isDashboardLayoutManager: true;
|
||||
/**
|
||||
* Notify the layout manager that the edit mode has changed
|
||||
* @param isEditing
|
||||
*/
|
||||
editModeChanged(isEditing: boolean): void;
|
||||
/**
|
||||
* Not sure we will need this in the long run, we should be able to handle this inside internally
|
||||
*/
|
||||
getNextPanelId(): number;
|
||||
/**
|
||||
* Remove an element / panel
|
||||
* @param element
|
||||
@ -54,7 +53,11 @@ export interface DashboardLayoutManager extends SceneObject {
|
||||
/**
|
||||
* Renders options and layout actions
|
||||
*/
|
||||
renderEditor?(): React.ReactNode;
|
||||
getOptions?(): OptionsPaneItemDescriptor[];
|
||||
}
|
||||
|
||||
export function isDashboardLayoutManager(obj: SceneObject): obj is DashboardLayoutManager {
|
||||
return 'isDashboardLayoutManager' in obj;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,10 +76,6 @@ export interface LayoutRegistryItem extends RegistryItem {
|
||||
createFromSaveModel?(saveModel: any): void;
|
||||
}
|
||||
|
||||
export interface LayoutEditorProps<T> {
|
||||
layoutManager: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface is needed to support layouts existing on different levels of the scene (DashboardScene and inside the TabsLayoutManager)
|
||||
*/
|
||||
|
@ -19,12 +19,14 @@ import {
|
||||
SceneDataLayerProvider,
|
||||
SceneDataLayerControls,
|
||||
UserActionEvent,
|
||||
SceneObjectState,
|
||||
} from '@grafana/scenes';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { DashboardDTO, DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
|
||||
import { DashboardEditPaneBehavior } from '../edit-pane/DashboardEditPaneBehavior';
|
||||
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
|
||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||
import { DashboardControls } from '../scene/DashboardControls';
|
||||
@ -214,6 +216,32 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
|
||||
});
|
||||
}
|
||||
|
||||
const behaviorList: SceneObjectState['$behaviors'] = [
|
||||
new behaviors.CursorSync({
|
||||
sync: oldModel.graphTooltip,
|
||||
}),
|
||||
new behaviors.SceneQueryController(),
|
||||
registerDashboardMacro,
|
||||
registerPanelInteractionsReporter,
|
||||
new DashboardEditPaneBehavior({}),
|
||||
new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }),
|
||||
preserveDashboardSceneStateInLocalStorage,
|
||||
addPanelsOnLoadBehavior,
|
||||
new DashboardScopesFacade({
|
||||
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange,
|
||||
uid: oldModel.uid,
|
||||
}),
|
||||
new DashboardReloadBehavior({
|
||||
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange,
|
||||
uid: oldModel.uid,
|
||||
version: oldModel.version,
|
||||
}),
|
||||
];
|
||||
|
||||
if (config.featureToggles.dashboardNewLayouts) {
|
||||
behaviorList.push(new DashboardEditPaneBehavior({}));
|
||||
}
|
||||
|
||||
const dashboardScene = new DashboardScene({
|
||||
description: oldModel.description,
|
||||
editable: oldModel.editable,
|
||||
@ -242,28 +270,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
|
||||
UNSAFE_nowDelay: oldModel.timepicker?.nowDelay,
|
||||
}),
|
||||
$variables: variables,
|
||||
$behaviors: [
|
||||
new behaviors.CursorSync({
|
||||
sync: oldModel.graphTooltip,
|
||||
}),
|
||||
new behaviors.SceneQueryController(),
|
||||
registerDashboardMacro,
|
||||
registerPanelInteractionsReporter,
|
||||
new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }),
|
||||
preserveDashboardSceneStateInLocalStorage,
|
||||
addPanelsOnLoadBehavior,
|
||||
new DashboardScopesFacade({
|
||||
reloadOnParamsChange:
|
||||
config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange,
|
||||
uid: oldModel.uid,
|
||||
}),
|
||||
new DashboardReloadBehavior({
|
||||
reloadOnParamsChange:
|
||||
config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange,
|
||||
uid: oldModel.uid,
|
||||
version: oldModel.version,
|
||||
}),
|
||||
],
|
||||
$behaviors: behaviorList,
|
||||
$data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }),
|
||||
controls: new DashboardControls({
|
||||
variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()],
|
||||
|
@ -34,7 +34,7 @@ import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLay
|
||||
|
||||
import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2';
|
||||
|
||||
function setupDashboardScene(state: DashboardSceneState): DashboardScene {
|
||||
function setupDashboardScene(state: Partial<DashboardSceneState>): DashboardScene {
|
||||
return new DashboardScene(state);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { VizPanelLinks } from '../scene/PanelLinks';
|
||||
|
||||
import { getLayoutManagerFor } from './utils';
|
||||
|
||||
function getTimePicker(scene: DashboardScene) {
|
||||
return scene.state.controls?.state.timePicker;
|
||||
}
|
||||
@ -53,4 +55,5 @@ export const dashboardSceneGraph = {
|
||||
getVizPanels,
|
||||
getDataLayers,
|
||||
getCursorSync,
|
||||
getLayoutManagerFor,
|
||||
};
|
||||
|
@ -18,6 +18,7 @@ import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
|
||||
import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types';
|
||||
|
||||
export const NEW_PANEL_HEIGHT = 8;
|
||||
export const NEW_PANEL_WIDTH = 12;
|
||||
@ -220,6 +221,7 @@ export function getDefaultVizPanel(): VizPanel {
|
||||
pluginId: 'timeseries',
|
||||
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
|
||||
hoverHeaderOffset: 0,
|
||||
$behaviors: [],
|
||||
menu: new VizPanelMenu({
|
||||
$behaviors: [panelMenuBehavior],
|
||||
}),
|
||||
@ -279,3 +281,16 @@ export function activateSceneObjectAndParentTree(so: SceneObject): CancelActivat
|
||||
* Useful when rendering a scene object out of context of it's parent
|
||||
*/
|
||||
export const activateInActiveParents = activateSceneObjectAndParentTree;
|
||||
|
||||
export function getLayoutManagerFor(sceneObject: SceneObject): DashboardLayoutManager {
|
||||
let parent = sceneObject.parent;
|
||||
|
||||
while (parent) {
|
||||
if (isDashboardLayoutManager(parent)) {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
throw new Error('Could not find layout manager for scene object');
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box } from '@grafana/ui';
|
||||
|
||||
import { OptionsPaneCategory } from './OptionsPaneCategory';
|
||||
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
|
||||
|
||||
@ -57,6 +59,10 @@ export class OptionsPaneCategoryDescriptor {
|
||||
return this.props.customRender();
|
||||
}
|
||||
|
||||
if (this.props.id === '') {
|
||||
return <Box padding={2}>{this.items.map((item) => item.render(searchQuery))}</Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionsPaneCategory key={this.props.title} {...this.props}>
|
||||
{this.items.map((item) => item.render(searchQuery))}
|
||||
|
Loading…
Reference in New Issue
Block a user