mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scenes: Add panel frame options and visualization options to panel editor (#80884)
This commit is contained in:
@@ -2554,16 +2554,7 @@ exports[`better eslint`] = {
|
|||||||
],
|
],
|
||||||
"public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx:5381": [
|
"public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx:5381": [
|
||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
|
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
|
||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"],
|
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"]
|
||||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "5"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "6"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "7"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "8"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "9"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "10"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor.tsx:5381": [
|
"public/app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
|||||||
@@ -138,6 +138,12 @@ export function buildPanelEditScene(panel: VizPanel): PanelEditor {
|
|||||||
body: new PanelOptionsPane(vizPanelMgr),
|
body: new PanelOptionsPane(vizPanelMgr),
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}),
|
}),
|
||||||
|
primaryPaneStyles: {
|
||||||
|
minWidth: '0',
|
||||||
|
},
|
||||||
|
secondaryPaneStyles: {
|
||||||
|
minWidth: '0',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
||||||
import { ButtonGroup, FilterInput, RadioButtonGroup, ToolbarButton, useStyles2 } from '@grafana/ui';
|
import { ButtonGroup, FilterInput, RadioButtonGroup, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||||
|
import { getPanelFrameCategory2 } from 'app/features/dashboard/components/PanelEditor/getPanelFrameOptions';
|
||||||
|
import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
|
||||||
import { getAllPanelPluginMeta } from 'app/features/panel/state/util';
|
import { getAllPanelPluginMeta } from 'app/features/panel/state/util';
|
||||||
|
|
||||||
import { PanelVizTypePicker } from './PanelVizTypePicker';
|
import { PanelVizTypePicker } from './PanelVizTypePicker';
|
||||||
@@ -24,9 +26,27 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
|||||||
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
|
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
|
||||||
const { panelManager } = model;
|
const { panelManager } = model;
|
||||||
const { panel } = panelManager.state;
|
const { panel } = panelManager.state;
|
||||||
const { pluginId } = panel.useState();
|
const { pluginId, options } = panel.useState();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [isVizPickerOpen, setVizPickerOpen] = useState(true);
|
const [isVizPickerOpen, setVizPickerOpen] = useState(true);
|
||||||
|
const panelFrameOptions = useMemo(() => getPanelFrameCategory2(panel), [panel]);
|
||||||
|
|
||||||
|
const visualizationOptions = useMemo(() => {
|
||||||
|
const plugin = panel.getPlugin();
|
||||||
|
if (!plugin) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getVisualizationOptions2({
|
||||||
|
panel,
|
||||||
|
plugin: plugin,
|
||||||
|
eventBus: panel.getPanelContext().eventBus,
|
||||||
|
instanceState: panel.getPanelContext().instanceState!,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [panel, options]);
|
||||||
|
|
||||||
|
const mainBoxElements = [panelFrameOptions.render(), ...(visualizationOptions?.map((v) => v.render()) ?? [])];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
@@ -44,18 +64,23 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
|||||||
)}
|
)}
|
||||||
{!isVizPickerOpen && (
|
{!isVizPickerOpen && (
|
||||||
<>
|
<>
|
||||||
<FilterInput value={''} placeholder="Search options" onChange={() => {}} />
|
<div className={styles.top}>
|
||||||
<RadioButtonGroup
|
<FilterInput
|
||||||
options={[
|
className={styles.searchOptions}
|
||||||
{ label: 'All', value: 'All' },
|
value={''}
|
||||||
{ label: 'Overrides', value: 'Overrides' },
|
placeholder="Search options"
|
||||||
]}
|
onChange={() => {}}
|
||||||
value={'All'}
|
/>
|
||||||
fullWidth
|
<RadioButtonGroup
|
||||||
></RadioButtonGroup>
|
options={[
|
||||||
{/* <OptionsPaneCategory id="test" title="Panel options">
|
{ label: 'All', value: 'All' },
|
||||||
Placeholder
|
{ label: 'Overrides', value: 'Overrides' },
|
||||||
</OptionsPaneCategory> */}
|
]}
|
||||||
|
value={'All'}
|
||||||
|
fullWidth
|
||||||
|
></RadioButtonGroup>
|
||||||
|
</div>
|
||||||
|
<div className={styles.mainBox}>{mainBoxElements}</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -66,20 +91,38 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
|||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2) {
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
return {
|
return {
|
||||||
|
top: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
border: `1px solid ${theme.colors.border.weak}`,
|
||||||
|
borderBottom: 'none',
|
||||||
|
borderTopLeftRadius: theme.shape.radius.default,
|
||||||
|
background: theme.colors.background.primary,
|
||||||
|
}),
|
||||||
box: css({
|
box: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexGrow: '1',
|
flexGrow: '1',
|
||||||
padding: theme.spacing(1),
|
|
||||||
background: theme.colors.background.primary,
|
background: theme.colors.background.primary,
|
||||||
border: `1px solid ${theme.colors.border.weak}`,
|
overflow: 'hidden',
|
||||||
gap: theme.spacing(1),
|
|
||||||
}),
|
}),
|
||||||
wrapper: css({
|
wrapper: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: theme.spacing(2),
|
|
||||||
flexGrow: '1',
|
flexGrow: '1',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
mainBox: css({
|
||||||
|
flexGrow: 1,
|
||||||
|
background: theme.colors.background.primary,
|
||||||
|
border: `1px solid ${theme.components.panel.borderColor}`,
|
||||||
|
borderTop: 'none',
|
||||||
|
overflow: 'auto',
|
||||||
|
}),
|
||||||
|
searchOptions: css({
|
||||||
|
minHeight: theme.spacing(4),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,13 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
padding: theme.spacing(1),
|
||||||
height: '100%',
|
height: '100%',
|
||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
|
border: `1px solid ${theme.colors.border.weak}`,
|
||||||
|
borderRight: 'none',
|
||||||
|
borderBottom: 'none',
|
||||||
|
borderTopLeftRadius: theme.shape.radius.default,
|
||||||
}),
|
}),
|
||||||
filter: css({
|
filter: css({
|
||||||
minHeight: theme.spacing(4),
|
minHeight: theme.spacing(4),
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const instance2SettingsMock = {
|
|||||||
jest.mock('app/core/store', () => ({
|
jest.mock('app/core/store', () => ({
|
||||||
exists: jest.fn(),
|
exists: jest.fn(),
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
getObject: jest.fn(),
|
getObject: jest.fn((_a, b) => b),
|
||||||
setObject: jest.fn(),
|
setObject: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { restore, versions } from './__mocks__/dashboardHistoryMocks';
|
|||||||
const getMock = jest.fn().mockResolvedValue({});
|
const getMock = jest.fn().mockResolvedValue({});
|
||||||
const postMock = jest.fn().mockResolvedValue({});
|
const postMock = jest.fn().mockResolvedValue({});
|
||||||
|
|
||||||
jest.mock('app/core/store');
|
jest.mock('app/core/store', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
getObject: jest.fn((_a, b) => b),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => {
|
jest.mock('@grafana/runtime', () => {
|
||||||
const original = jest.requireActual('@grafana/runtime');
|
const original = jest.requireActual('@grafana/runtime');
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { DashboardExporter, LibraryElementExport } from './DashboardExporter';
|
|||||||
jest.mock('app/core/store', () => {
|
jest.mock('app/core/store', () => {
|
||||||
return {
|
return {
|
||||||
getBool: jest.fn(),
|
getBool: jest.fn(),
|
||||||
getObject: jest.fn(),
|
getObject: jest.fn((_a, b) => b),
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export interface OptionsPaneCategoryProps {
|
|||||||
sandboxId?: string;
|
sandboxId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CATEGORY_PARAM_NAME = 'showCategory';
|
const CATEGORY_PARAM_NAME = 'showCategory' as const;
|
||||||
|
|
||||||
export const OptionsPaneCategory = React.memo(
|
export const OptionsPaneCategory = React.memo(
|
||||||
({
|
({
|
||||||
@@ -30,23 +30,21 @@ export const OptionsPaneCategory = React.memo(
|
|||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
forceOpen,
|
forceOpen,
|
||||||
isOpenDefault,
|
isOpenDefault = true,
|
||||||
renderTitle,
|
renderTitle,
|
||||||
className,
|
className,
|
||||||
itemsCount,
|
itemsCount,
|
||||||
isNested = false,
|
isNested = false,
|
||||||
sandboxId,
|
sandboxId,
|
||||||
}: OptionsPaneCategoryProps) => {
|
}: OptionsPaneCategoryProps) => {
|
||||||
const initialIsExpanded = isOpenDefault !== false;
|
|
||||||
const [savedState, setSavedState] = useLocalStorage(getOptionGroupStorageKey(id), {
|
const [savedState, setSavedState] = useLocalStorage(getOptionGroupStorageKey(id), {
|
||||||
isExpanded: initialIsExpanded,
|
isExpanded: isOpenDefault,
|
||||||
});
|
});
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const [isExpanded, setIsExpanded] = useState(savedState?.isExpanded ?? isOpenDefault);
|
||||||
const [queryParams, updateQueryParams] = useQueryParams();
|
|
||||||
const [isExpanded, setIsExpanded] = useState(savedState?.isExpanded ?? initialIsExpanded);
|
|
||||||
const manualClickTime = useRef(0);
|
const manualClickTime = useRef(0);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [queryParams, updateQueryParams] = useQueryParams();
|
||||||
const isOpenFromUrl = queryParams[CATEGORY_PARAM_NAME] === id;
|
const isOpenFromUrl = queryParams[CATEGORY_PARAM_NAME] === id;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -92,13 +90,13 @@ export const OptionsPaneCategory = React.memo(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
const boxStyles = cx(
|
const boxStyles = cx(
|
||||||
{
|
{
|
||||||
[styles.box]: true,
|
[styles.box]: true,
|
||||||
[styles.boxNestedExpanded]: isNested && isExpanded,
|
[styles.boxNestedExpanded]: isNested && isExpanded,
|
||||||
},
|
},
|
||||||
className,
|
className
|
||||||
'options-group'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const headerStyles = cx(styles.header, {
|
const headerStyles = cx(styles.header, {
|
||||||
@@ -149,61 +147,60 @@ export const OptionsPaneCategory = React.memo(
|
|||||||
|
|
||||||
OptionsPaneCategory.displayName = 'OptionsPaneCategory';
|
OptionsPaneCategory.displayName = 'OptionsPaneCategory';
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
return {
|
box: css({
|
||||||
box: css`
|
borderTop: `1px solid ${theme.colors.border.weak}`,
|
||||||
border-top: 1px solid ${theme.colors.border.weak};
|
}),
|
||||||
`,
|
boxNestedExpanded: css({
|
||||||
boxNestedExpanded: css`
|
marginBottom: theme.spacing(2),
|
||||||
margin-bottom: ${theme.spacing(2)};
|
}),
|
||||||
`,
|
title: css({
|
||||||
title: css`
|
flexGrow: 1,
|
||||||
flex-grow: 1;
|
overflow: 'hidden',
|
||||||
overflow: hidden;
|
lineHeight: 1.5,
|
||||||
line-height: 1.5;
|
fontSize: '1rem',
|
||||||
font-size: 1rem;
|
paddingLeft: '6px',
|
||||||
padding-left: 6px;
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
font-weight: ${theme.typography.fontWeightMedium};
|
margin: 0,
|
||||||
margin: 0;
|
}),
|
||||||
`,
|
header: css({
|
||||||
header: css`
|
display: 'flex',
|
||||||
display: flex;
|
cursor: 'pointer',
|
||||||
cursor: pointer;
|
alignItems: 'center',
|
||||||
align-items: center;
|
padding: theme.spacing(0.5),
|
||||||
padding: ${theme.spacing(0.5)};
|
color: theme.colors.text.primary,
|
||||||
color: ${theme.colors.text.primary};
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
font-weight: ${theme.typography.fontWeightMedium};
|
|
||||||
|
|
||||||
&:hover {
|
'&:hover': {
|
||||||
background: ${theme.colors.emphasize(theme.colors.background.primary, 0.03)};
|
background: theme.colors.emphasize(theme.colors.background.primary, 0.03),
|
||||||
}
|
},
|
||||||
`,
|
}),
|
||||||
toggleButton: css`
|
toggleButton: css({
|
||||||
align-self: baseline;
|
alignSelf: 'baseline',
|
||||||
`,
|
}),
|
||||||
headerExpanded: css`
|
headerExpanded: css({
|
||||||
color: ${theme.colors.text.primary};
|
color: theme.colors.text.primary,
|
||||||
`,
|
}),
|
||||||
headerNested: css`
|
headerNested: css({
|
||||||
padding: ${theme.spacing(0.5, 0, 0.5, 0)};
|
padding: theme.spacing(0.5, 0, 0.5, 0),
|
||||||
`,
|
}),
|
||||||
body: css`
|
body: css({
|
||||||
padding: ${theme.spacing(1, 2, 1, 4)};
|
padding: theme.spacing(1, 2, 1, 4),
|
||||||
`,
|
}),
|
||||||
bodyNested: css`
|
bodyNested: css({
|
||||||
position: relative;
|
position: 'relative',
|
||||||
padding-right: 0;
|
paddingRight: 0,
|
||||||
&:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 8px;
|
|
||||||
width: 1px;
|
|
||||||
height: 100%;
|
|
||||||
background: ${theme.colors.border.weak};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOptionGroupStorageKey = (id: string): string => `${PANEL_EDITOR_UI_STATE_STORAGE_KEY}.optionGroup[${id}]`;
|
'&:before': {
|
||||||
|
content: "''",
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: '8px',
|
||||||
|
width: '1px',
|
||||||
|
height: '100%',
|
||||||
|
background: theme.colors.border.weak,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getOptionGroupStorageKey = (id: string) => `${PANEL_EDITOR_UI_STATE_STORAGE_KEY}.optionGroup[${id}]`;
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ export interface OptionsPaneCategoryDescriptorProps {
|
|||||||
customRender?: () => React.ReactNode;
|
customRender?: () => React.ReactNode;
|
||||||
sandboxId?: string;
|
sandboxId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is not a real React component but an intermediary to enable deep option search without traversing a React node tree.
|
* This is not a real React component but an intermediary to enable deep option search without traversing a React node tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class OptionsPaneCategoryDescriptor {
|
export class OptionsPaneCategoryDescriptor {
|
||||||
items: OptionsPaneItemDescriptor[] = [];
|
items: OptionsPaneItemDescriptor[] = [];
|
||||||
categories: OptionsPaneCategoryDescriptor[] = [];
|
categories: OptionsPaneCategoryDescriptor[] = [];
|
||||||
@@ -41,14 +41,14 @@ export class OptionsPaneCategoryDescriptor {
|
|||||||
|
|
||||||
getCategory(name: string): OptionsPaneCategoryDescriptor {
|
getCategory(name: string): OptionsPaneCategoryDescriptor {
|
||||||
let sub = this.categories.find((c) => c.props.id === name);
|
let sub = this.categories.find((c) => c.props.id === name);
|
||||||
if (sub) {
|
if (!sub) {
|
||||||
return sub;
|
sub = new OptionsPaneCategoryDescriptor({
|
||||||
|
title: name,
|
||||||
|
id: name,
|
||||||
|
});
|
||||||
|
this.addCategory(sub);
|
||||||
}
|
}
|
||||||
sub = new OptionsPaneCategoryDescriptor({
|
|
||||||
title: name,
|
|
||||||
id: name,
|
|
||||||
});
|
|
||||||
this.addCategory(sub);
|
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { VizPanel } from '@grafana/scenes';
|
||||||
import { DataLinksInlineEditor, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui';
|
import { DataLinksInlineEditor, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui';
|
||||||
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||||
|
|
||||||
@@ -171,3 +172,150 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPanelFrameCategory2(panel: VizPanel): OptionsPaneCategoryDescriptor {
|
||||||
|
const descriptor = new OptionsPaneCategoryDescriptor({
|
||||||
|
title: 'Panel options',
|
||||||
|
id: 'Panel options',
|
||||||
|
isOpenDefault: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return descriptor
|
||||||
|
.addItem(
|
||||||
|
new OptionsPaneItemDescriptor({
|
||||||
|
title: 'Title',
|
||||||
|
value: panel.state.title,
|
||||||
|
popularRank: 1,
|
||||||
|
render: function renderTitle() {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
id="PanelFrameTitle"
|
||||||
|
defaultValue={panel.state.title}
|
||||||
|
onBlur={(e) => panel.setState({ title: e.currentTarget.value })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// addon: config.featureToggles.dashgpt && <GenAIPanelTitleButton onGenerate={setPanelTitle} panel={panel} />,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.addItem(
|
||||||
|
new OptionsPaneItemDescriptor({
|
||||||
|
title: 'Description',
|
||||||
|
description: panel.state.description,
|
||||||
|
value: panel.state.description,
|
||||||
|
render: function renderDescription() {
|
||||||
|
return (
|
||||||
|
<TextArea
|
||||||
|
id="description-text-area"
|
||||||
|
defaultValue={panel.state.description}
|
||||||
|
onBlur={(e) => panel.setState({ description: e.currentTarget.value })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// addon: config.featureToggles.dashgpt && (
|
||||||
|
// <GenAIPanelDescriptionButton onGenerate={setPanelDescription} panel={panel} />
|
||||||
|
// ),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.addItem(
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// .addCategory(
|
||||||
|
// new OptionsPaneCategoryDescriptor({
|
||||||
|
// title: 'Panel links',
|
||||||
|
// id: 'Panel links',
|
||||||
|
// isOpenDefault: false,
|
||||||
|
// itemsCount: panel.state.links?.length,
|
||||||
|
// }).addItem(
|
||||||
|
// new OptionsPaneItemDescriptor({
|
||||||
|
// title: 'Panel links',
|
||||||
|
// render: function renderLinks() {
|
||||||
|
// return (
|
||||||
|
// <DataLinksInlineEditor
|
||||||
|
// links={panel.links}
|
||||||
|
// onChange={(links) => onPanelConfigChange('links', links)}
|
||||||
|
// getSuggestions={getPanelLinksVariableSuggestions}
|
||||||
|
// data={[]}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// .addCategory(
|
||||||
|
// new OptionsPaneCategoryDescriptor({
|
||||||
|
// title: 'Repeat options',
|
||||||
|
// id: 'Repeat options',
|
||||||
|
// isOpenDefault: false,
|
||||||
|
// })
|
||||||
|
// .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 (
|
||||||
|
// <RepeatRowSelect
|
||||||
|
// id="repeat-by-variable-select"
|
||||||
|
// repeat={panel.repeat}
|
||||||
|
// onChange={(value?: string) => {
|
||||||
|
// onPanelConfigChange('repeat', value);
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// .addItem(
|
||||||
|
// new OptionsPaneItemDescriptor({
|
||||||
|
// title: 'Repeat direction',
|
||||||
|
// showIf: () => !!panel.repeat,
|
||||||
|
// render: function renderRepeatOptions() {
|
||||||
|
// const directionOptions = [
|
||||||
|
// { label: 'Horizontal', value: 'h' },
|
||||||
|
// { label: 'Vertical', value: 'v' },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <RadioButtonGroup
|
||||||
|
// options={directionOptions}
|
||||||
|
// value={panel.repeatDirection || 'h'}
|
||||||
|
// onChange={(value) => onPanelConfigChange('repeatDirection', value)}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// .addItem(
|
||||||
|
// new OptionsPaneItemDescriptor({
|
||||||
|
// title: 'Max per row',
|
||||||
|
// showIf: () => Boolean(panel.repeat && panel.repeatDirection === 'h'),
|
||||||
|
// render: function renderOption() {
|
||||||
|
// const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
||||||
|
// return (
|
||||||
|
// <Select
|
||||||
|
// options={maxPerRowOptions}
|
||||||
|
// value={panel.maxPerRow}
|
||||||
|
// onChange={(value) => onPanelConfigChange('maxPerRow', value.value)}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
EventBus,
|
EventBus,
|
||||||
InterpolateFunction,
|
InterpolateFunction,
|
||||||
PanelData,
|
PanelData,
|
||||||
|
PanelPlugin,
|
||||||
StandardEditorContext,
|
StandardEditorContext,
|
||||||
VariableSuggestionsScope,
|
VariableSuggestionsScope,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
NestedValueAccess,
|
NestedValueAccess,
|
||||||
PanelOptionsEditorBuilder,
|
PanelOptionsEditorBuilder,
|
||||||
} from '@grafana/data/src/utils/OptionsUIBuilders';
|
} from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||||
|
import { VizPanel } from '@grafana/scenes';
|
||||||
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||||
|
|
||||||
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
||||||
@@ -146,6 +148,100 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
|||||||
return Object.values(categoryIndex);
|
return Object.values(categoryIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OptionPaneRenderProps2 {
|
||||||
|
panel: VizPanel;
|
||||||
|
eventBus: EventBus;
|
||||||
|
plugin: PanelPlugin;
|
||||||
|
data?: PanelData;
|
||||||
|
instanceState: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVisualizationOptions2(props: OptionPaneRenderProps2): OptionsPaneCategoryDescriptor[] {
|
||||||
|
const { plugin, panel, data, eventBus, instanceState } = props;
|
||||||
|
|
||||||
|
const categoryIndex: Record<string, OptionsPaneCategoryDescriptor> = {};
|
||||||
|
const getOptionsPaneCategory = (categoryNames?: string[]): OptionsPaneCategoryDescriptor => {
|
||||||
|
const categoryName = categoryNames?.[0] ?? plugin.meta.name;
|
||||||
|
const category = categoryIndex[categoryName];
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (categoryIndex[categoryName] = new OptionsPaneCategoryDescriptor({
|
||||||
|
title: categoryName,
|
||||||
|
id: categoryName,
|
||||||
|
sandboxId: plugin.meta.id,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentOptions = panel.state.options;
|
||||||
|
const access: NestedValueAccess = {
|
||||||
|
getValue: (path) => lodashGet(currentOptions, path),
|
||||||
|
onChange: (path, value) => {
|
||||||
|
const newOptions = setOptionImmutably(currentOptions, path, value);
|
||||||
|
panel.onOptionsChange(newOptions);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = getStandardEditorContext({
|
||||||
|
data,
|
||||||
|
replaceVariables: panel.interpolate,
|
||||||
|
options: currentOptions,
|
||||||
|
eventBus: eventBus,
|
||||||
|
instanceState,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the options into categories
|
||||||
|
fillOptionsPaneItems(plugin.getPanelOptionsSupplier(), access, getOptionsPaneCategory, context);
|
||||||
|
|
||||||
|
// Field options
|
||||||
|
const currentFieldConfig = panel.state.fieldConfig;
|
||||||
|
for (const fieldOption of plugin.fieldConfigRegistry.list()) {
|
||||||
|
const hideOption =
|
||||||
|
fieldOption.showIf &&
|
||||||
|
(fieldOption.isCustom
|
||||||
|
? !fieldOption.showIf(currentFieldConfig.defaults.custom, data?.series)
|
||||||
|
: !fieldOption.showIf(currentFieldConfig.defaults, data?.series));
|
||||||
|
if (fieldOption.hideFromDefaults || hideOption) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = getOptionsPaneCategory(fieldOption.category);
|
||||||
|
const Editor = fieldOption.editor;
|
||||||
|
|
||||||
|
const defaults = currentFieldConfig.defaults;
|
||||||
|
const value = fieldOption.isCustom
|
||||||
|
? defaults.custom
|
||||||
|
? lodashGet(defaults.custom, fieldOption.path)
|
||||||
|
: undefined
|
||||||
|
: lodashGet(defaults, fieldOption.path);
|
||||||
|
|
||||||
|
if (fieldOption.getItemsCount) {
|
||||||
|
category.props.itemsCount = fieldOption.getItemsCount(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
category.addItem(
|
||||||
|
new OptionsPaneItemDescriptor({
|
||||||
|
title: fieldOption.name,
|
||||||
|
description: fieldOption.description,
|
||||||
|
overrides: getOptionOverrides(fieldOption, currentFieldConfig, data?.series),
|
||||||
|
render: function renderEditor() {
|
||||||
|
const onChange = (v: unknown) => {
|
||||||
|
panel.onFieldConfigChange(
|
||||||
|
updateDefaultFieldConfigValue(currentFieldConfig, fieldOption.path, v, fieldOption.isCustom)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Editor value={value} onChange={onChange} item={fieldOption} context={context} id={fieldOption.id} />;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(categoryIndex);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will iterate all options panes and add register them with the configured categories
|
* This will iterate all options panes and add register them with the configured categories
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
// Mock the store module
|
// Mock the store module
|
||||||
jest.mock('app/core/store', () => ({
|
jest.mock('app/core/store', () => ({
|
||||||
exists: jest.fn(),
|
exists: jest.fn(),
|
||||||
getObject: jest.fn(),
|
getObject: jest.fn((_a, b) => b),
|
||||||
setObject: jest.fn(),
|
setObject: jest.fn(),
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user