XYChart: Config cleanup and refactoring (#79915)

* xy config cleanup

* more cleanup

* clean up betterer some

---------

Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
Ryan McKinley
2023-12-31 16:50:53 -08:00
committed by GitHub
co-authored by nmarrs
parent 7eea30d0e8
commit ae3156d727
13 changed files with 265 additions and 145 deletions
+1 -16
View File
@@ -6596,22 +6596,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/plugins/panel/xychart/ManualEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Styles should be written using objects.", "12"],
[0, 0, 0, "Styles should be written using objects.", "13"],
[0, 0, 0, "Styles should be written using objects.", "14"],
[0, 0, 0, "Styles should be written using objects.", "15"]
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/xychart/TooltipView.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
@@ -18,14 +18,14 @@ title: XYChartPanelCfg kind
| Property | Type | Required | Default | Description |
|-----------------------|--------------------------------|----------|---------|---------------------------------------------------------|
| `FieldConfig` | [object](#fieldconfig) | **Yes** | | |
| `Options` | [object](#options) | **Yes** | | |
| `ScatterSeriesConfig` | [object](#scatterseriesconfig) | **Yes** | | |
| `ScatterShow` | string | **Yes** | | Possible values are: `points`, `lines`, `points+lines`. |
| `SeriesMapping` | string | **Yes** | | Possible values are: `auto`, `manual`. |
| `XYDimensionConfig` | [object](#xydimensionconfig) | **Yes** | | |
| Property | Type | Required | Default | Description |
|-----------------------|--------------------------------|----------|---------|----------------------------------------------------------------------|
| `FieldConfig` | [object](#fieldconfig) | **Yes** | | |
| `Options` | [object](#options) | **Yes** | | |
| `ScatterSeriesConfig` | [object](#scatterseriesconfig) | **Yes** | | |
| `ScatterShow` | string | **Yes** | | Possible values are: `points`, `lines`, `points+lines`. |
| `SeriesMapping` | string | **Yes** | | Auto is "table" in the UI<br/>Possible values are: `auto`, `manual`. |
| `XYDimensionConfig` | [object](#xydimensionconfig) | **Yes** | | Configuration for the Table/Auto mode |
### FieldConfig
@@ -150,11 +150,11 @@ It extends [OptionsWithLegend](#optionswithlegend) and [OptionsWithTooltip](#opt
| Property | Type | Required | Default | Description |
|-----------------|-----------------------------------------------|----------|---------|----------------------------------------------------------------------------|
| `dims` | [XYDimensionConfig](#xydimensionconfig) | **Yes** | | |
| `dims` | [XYDimensionConfig](#xydimensionconfig) | **Yes** | | Configuration for the Table/Auto mode |
| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | *(Inherited from [OptionsWithLegend](#optionswithlegend))*<br/>TODO docs |
| `series` | [ScatterSeriesConfig](#scatterseriesconfig)[] | **Yes** | | |
| `series` | [ScatterSeriesConfig](#scatterseriesconfig)[] | **Yes** | | Manual Mode |
| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | *(Inherited from [OptionsWithTooltip](#optionswithtooltip))*<br/>TODO docs |
| `seriesMapping` | string | No | | Possible values are: `auto`, `manual`. |
| `seriesMapping` | string | No | | Auto is "table" in the UI<br/>Possible values are: `auto`, `manual`. |
### OptionsWithLegend
@@ -228,6 +228,8 @@ It extends [FieldConfig](#fieldconfig).
### XYDimensionConfig
Configuration for the Table/Auto mode
| Property | Type | Required | Default | Description |
|-----------|----------|----------|---------|-----------------------------------|
| `frame` | integer | **Yes** | | Constraint: `>=0 & <=2147483647`. |
@@ -174,6 +174,12 @@ export interface StatsPickerConfigSettings {
defaultStat?: string;
}
export enum FieldNamePickerBaseNameMode {
IncludeAll = 'all',
ExcludeBaseNames = 'exclude',
OnlyBaseNames = 'only',
}
export interface FieldNamePickerConfigSettings {
/**
* Function is a predicate, to test each element of the array.
@@ -186,11 +192,16 @@ export interface FieldNamePickerConfigSettings {
*/
noFieldsMessage?: string;
/**addFieldNamePicker
/**
* Sets the width to a pixel value.
*/
width?: number;
/**
* Exclude names that can match a collection of values
*/
baseNameMode?: FieldNamePickerBaseNameMode;
/**
* Placeholder text to display when nothing is selected.
*/
@@ -13,6 +13,9 @@ import * as common from '@grafana/schema';
export const pluginVersion = "10.3.0-pre";
/**
* Auto is "table" in the UI
*/
export enum SeriesMapping {
Auto = 'auto',
Manual = 'manual',
@@ -24,6 +27,9 @@ export enum ScatterShow {
PointsAndLines = 'points+lines',
}
/**
* Configuration for the Table/Auto mode
*/
export interface XYDimensionConfig {
exclude?: Array<string>;
frame: number;
@@ -57,7 +63,13 @@ export interface ScatterSeriesConfig extends FieldConfig {
}
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip {
/**
* Table Mode (auto)
*/
dims: XYDimensionConfig;
/**
* Manual Mode
*/
series: Array<ScatterSeriesConfig>;
seriesMapping?: SeriesMapping;
}
@@ -12,7 +12,7 @@ type Props = StandardEditorProps<string, FieldNamePickerConfigSettings>;
export const FieldNamePicker = ({ value, onChange, context, item }: Props) => {
const settings: FieldNamePickerConfigSettings = item.settings ?? {};
const names = useFieldDisplayNames(context.data, settings?.filter);
const selectOptions = useSelectOptions(names, value);
const selectOptions = useSelectOptions(names, value, undefined, undefined, settings.baseNameMode);
const onSelectChange = useCallback(
(selection?: SelectableValue<string>) => {
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { DataFrame, Field, getFieldDisplayName, SelectableValue } from '@grafana/data';
import { DataFrame, Field, getFieldDisplayName, SelectableValue, FieldNamePickerBaseNameMode } from '@grafana/data';
import { getFieldTypeIcon } from '../../types';
@@ -74,7 +74,8 @@ export function useSelectOptions(
displayNames: FrameFieldsDisplayNames,
currentName?: string,
firstItem?: SelectableValue<string>,
fieldType?: string
fieldType?: string,
baseNameMode?: FieldNamePickerBaseNameMode
): Array<SelectableValue<string>> {
return useMemo(() => {
let found = false;
@@ -82,21 +83,8 @@ export function useSelectOptions(
if (firstItem) {
options.push(firstItem);
}
for (const name of displayNames.display) {
if (!found && name === currentName) {
found = true;
}
const field = displayNames.fields.get(name);
if (!fieldType || fieldType === field?.type) {
options.push({
value: name,
label: name,
icon: field ? getFieldTypeIcon(field) : undefined,
});
}
}
for (const name of displayNames.raw) {
if (!displayNames.display.has(name)) {
if (baseNameMode === FieldNamePickerBaseNameMode.OnlyBaseNames) {
for (const name of displayNames.raw) {
if (!found && name === currentName) {
found = true;
}
@@ -105,6 +93,34 @@ export function useSelectOptions(
label: `${name} (base field name)`,
});
}
} else {
for (const name of displayNames.display) {
if (!found && name === currentName) {
found = true;
}
const field = displayNames.fields.get(name);
if (!fieldType || fieldType === field?.type) {
options.push({
value: name,
label: name,
icon: field ? getFieldTypeIcon(field) : undefined,
});
}
}
if (baseNameMode !== FieldNamePickerBaseNameMode.ExcludeBaseNames) {
for (const name of displayNames.raw) {
if (!displayNames.display.has(name)) {
if (!found && name === currentName) {
found = true;
}
options.push({
value: name,
label: `${name} (base field name)`,
});
}
}
}
}
if (currentName && !found) {
@@ -114,5 +130,5 @@ export function useSelectOptions(
});
}
return options;
}, [displayNames, currentName, firstItem, fieldType]);
}, [displayNames, currentName, firstItem, fieldType, baseNameMode]);
}
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import React, { useCallback } from 'react';
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
import { GrafanaTheme2, SelectableValue, StandardEditorProps, FieldNamePickerBaseNameMode } from '@grafana/data';
import { ColorDimensionConfig } from '@grafana/schema';
import { Select, ColorPicker, useStyles2 } from '@grafana/ui';
import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/src/components/MatchersUI/utils';
@@ -11,19 +11,30 @@ const fixedColorOption: SelectableValue<string> = {
value: '_____fixed_____',
};
export const ColorDimensionEditor = (props: StandardEditorProps<ColorDimensionConfig>) => {
const { value, context, onChange } = props;
interface ColorDimensionSettings {
isClearable?: boolean;
baseNameMode?: FieldNamePickerBaseNameMode;
placeholder?: string;
}
export const ColorDimensionEditor = (props: StandardEditorProps<ColorDimensionConfig, ColorDimensionSettings>) => {
const { value, context, onChange, item } = props;
const defaultColor = 'dark-green';
const styles = useStyles2(getStyles);
const fieldName = value?.field;
const isFixed = Boolean(!fieldName);
const isFixed = value && Boolean(!fieldName) && value?.fixed;
const names = useFieldDisplayNames(context.data);
const selectOptions = useSelectOptions(names, fieldName, fixedColorOption);
const selectOptions = useSelectOptions(names, fieldName, fixedColorOption, undefined, item.settings?.baseNameMode);
const onSelectChange = useCallback(
(selection: SelectableValue<string>) => {
if (!selection) {
onChange(undefined);
return;
}
const field = selection.value;
if (field && field !== fixedColorOption.value) {
onChange({
@@ -61,10 +72,12 @@ export const ColorDimensionEditor = (props: StandardEditorProps<ColorDimensionCo
options={selectOptions}
onChange={onSelectChange}
noOptionsMessage="No fields found"
isClearable={item.settings?.isClearable}
placeholder={item.settings?.placeholder}
/>
{isFixed && (
<div className={styles.picker}>
<ColorPicker color={value?.fixed ?? defaultColor} onChange={onColorChange} enableNamedColors={true} />
<ColorPicker color={value?.fixed} onChange={onColorChange} enableNamedColors={true} />
</div>
)}
</div>
+12 -15
View File
@@ -15,7 +15,7 @@ import { XYDimensionConfig, Options } from './panelcfg.gen';
interface XYInfo {
numberFields: Array<SelectableValue<string>>;
xAxis: SelectableValue<string>;
xAxis?: SelectableValue<string>;
yFields: Array<SelectableValue<boolean>>;
}
@@ -24,7 +24,7 @@ export const AutoEditor = ({ value, onChange, context }: StandardEditorProps<XYD
if (context?.data?.length) {
return context.data.map((f, idx) => ({
value: idx,
label: getFrameDisplayName(f, idx),
label: `${getFrameDisplayName(f, idx)} (index: ${idx}, rows: ${f.length})`,
}));
}
return [{ value: 0, label: 'First result' }];
@@ -33,19 +33,15 @@ export const AutoEditor = ({ value, onChange, context }: StandardEditorProps<XYD
const dims = useMemo(() => getXYDimensions(value, context.data), [context.data, value]);
const info = useMemo(() => {
const first = {
label: '?',
value: undefined, // empty
};
const v: XYInfo = {
numberFields: [first],
numberFields: [],
yFields: [],
xAxis: value?.x
? {
label: `${value.x} (Not found)`,
value: value.x, // empty
}
: first,
: undefined,
};
const frame = context.data ? context.data[value?.frame ?? 0] : undefined;
if (frame) {
@@ -58,9 +54,6 @@ export const AutoEditor = ({ value, onChange, context }: StandardEditorProps<XYD
value: name,
};
v.numberFields.push(sel);
if (first.label === '?') {
first.label = `${name} (First)`;
}
if (value?.x && name === value.x) {
v.xAxis = sel;
}
@@ -79,7 +72,7 @@ export const AutoEditor = ({ value, onChange, context }: StandardEditorProps<XYD
const styles = useStyles2(getStyles);
if (!context.data) {
if (!context.data?.length) {
return <div>No data...</div>;
}
@@ -87,24 +80,28 @@ export const AutoEditor = ({ value, onChange, context }: StandardEditorProps<XYD
<div>
<Field label={'Data'}>
<Select
isClearable={true}
options={frameNames}
value={frameNames.find((v) => v.value === value?.frame) ?? frameNames[0]}
placeholder={frameNames[0].label}
value={frameNames.find((v) => v.value === value?.frame)}
onChange={(v) => {
onChange({
...value,
frame: v.value!,
frame: v?.value!,
});
}}
/>
</Field>
<Field label={'X Field'}>
<Select
isClearable={true}
options={info.numberFields}
value={info.xAxis}
placeholder={`${info.numberFields?.[0].label} (First numeric)`}
onChange={(v) => {
onChange({
...value,
x: v.value,
x: v?.value,
});
}}
/>
@@ -1,23 +1,27 @@
import { css, cx } from '@emotion/css';
import React, { useState, useEffect } from 'react';
import { GrafanaTheme2, StandardEditorProps } from '@grafana/data';
import { Button, Field, IconButton, useStyles2 } from '@grafana/ui';
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
import {
GrafanaTheme2,
StandardEditorProps,
FieldNamePickerBaseNameMode,
StandardEditorsRegistryItem,
} from '@grafana/data';
import { Button, IconButton, useStyles2 } from '@grafana/ui';
import { LayerName } from 'app/core/components/Layers/LayerName';
import { ColorDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors';
import { ScatterSeriesEditor } from './ScatterSeriesEditor';
import { Options, ScatterSeriesConfig, defaultFieldConfig } from './panelcfg.gen';
export const ManualEditor = ({
value,
onChange,
context,
}: StandardEditorProps<ScatterSeriesConfig[], any, Options>) => {
}: StandardEditorProps<ScatterSeriesConfig[], unknown, Options>) => {
const [selected, setSelected] = useState(0);
const style = useStyles2(getStyles);
const onFieldChange = (val: any | undefined, index: number, field: string) => {
const onFieldChange = (val: unknown | undefined, index: number, field: string) => {
onChange(
value.map((obj, i) => {
if (i === index) {
@@ -32,7 +36,7 @@ export const ManualEditor = ({
onChange([
...value,
{
pointColor: {} as any,
pointColor: undefined,
pointSize: defaultFieldConfig.pointSize,
},
]);
@@ -97,77 +101,58 @@ export const ManualEditor = ({
</div>
{selected >= 0 && value[selected] && (
<>
<div key={`series/${selected}`}>
<Field label={'X Field'}>
<FieldNamePicker
value={value[selected].x ?? ''}
context={context}
onChange={(field) => onFieldChange(field, selected, 'x')}
item={{} as any}
/>
</Field>
<Field label={'Y Field'}>
<FieldNamePicker
value={value[selected].y ?? ''}
context={context}
onChange={(field) => onFieldChange(field, selected, 'y')}
item={{} as any}
/>
</Field>
<Field label={'Point color'}>
<ColorDimensionEditor
value={value[selected].pointColor!}
context={context}
onChange={(field) => onFieldChange(field, selected, 'pointColor')}
item={{} as any}
/>
</Field>
<Field label={'Point size'}>
<ScaleDimensionEditor
value={value[selected].pointSize!}
context={context}
onChange={(field) => onFieldChange(field, selected, 'pointSize')}
item={{ settings: { min: 1, max: 100 } } as any}
/>
</Field>
</div>
</>
<ScatterSeriesEditor
key={`series/${selected}`}
baseNameMode={FieldNamePickerBaseNameMode.ExcludeBaseNames}
item={{} as StandardEditorsRegistryItem}
context={context}
value={value[selected]}
onChange={(v) => {
onChange(
value.map((obj, i) => {
if (i === selected) {
return v!;
}
return obj;
})
);
}}
/>
)}
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
marginBot: css`
margin-bottom: 20px;
`,
row: css`
padding: ${theme.spacing(0.5, 1)};
border-radius: ${theme.shape.radius.default};
background: ${theme.colors.background.secondary};
min-height: ${theme.spacing(4)};
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 3px;
cursor: pointer;
marginBot: css({
marginBottom: '20px',
}),
row: css({
padding: `${theme.spacing(0.5, 1)}`,
borderRadius: `${theme.shape.radius.default}`,
background: `${theme.colors.background.secondary}`,
minHeight: `${theme.spacing(4)}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '3px',
cursor: 'pointer',
border: 1px solid ${theme.components.input.borderColor};
&:hover {
border: 1px solid ${theme.components.input.borderHover};
}
`,
sel: css`
border: 1px solid ${theme.colors.primary.border};
&:hover {
border: 1px solid ${theme.colors.primary.border};
}
`,
actionIcon: css`
color: ${theme.colors.text.secondary};
&:hover {
color: ${theme.colors.text};
}
`,
border: `1px solid ${theme.components.input.borderColor}`,
'&:hover': {
border: `1px solid ${theme.components.input.borderHover}`,
},
}),
sel: css({
border: `1px solid ${theme.colors.primary.border}`,
'&:hover': {
border: `1px solid ${theme.colors.primary.border}`,
},
}),
actionIcon: css({
color: `${theme.colors.text.secondary}`,
'&:hover': {
color: `${theme.colors.text}`,
},
}),
});
@@ -0,0 +1,82 @@
import React from 'react';
import { StandardEditorProps, FieldNamePickerBaseNameMode } from '@grafana/data';
import { Field } from '@grafana/ui';
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
import { ColorDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors';
import { Options, ScatterSeriesConfig } from './panelcfg.gen';
export interface Props extends StandardEditorProps<ScatterSeriesConfig, unknown, Options> {
baseNameMode: FieldNamePickerBaseNameMode;
}
export const ScatterSeriesEditor = ({ value, onChange, context, baseNameMode }: Props) => {
const onFieldChange = (val: unknown | undefined, field: string) => {
onChange({ ...value, [field]: val });
};
return (
<div>
<Field label={'X Field'}>
<FieldNamePicker
value={value.x ?? ''}
context={context}
onChange={(field) => onFieldChange(field, 'x')}
item={{
id: 'x',
name: 'x',
settings: {
baseNameMode,
},
}}
/>
</Field>
<Field label={'Y Field'}>
<FieldNamePicker
value={value.y ?? ''}
context={context}
onChange={(field) => onFieldChange(field, 'y')}
item={{
id: 'x',
name: 'x',
settings: {
baseNameMode,
},
}}
/>
</Field>
<Field label={'Point color'}>
<ColorDimensionEditor
value={value.pointColor!}
context={context}
onChange={(field) => onFieldChange(field, 'pointColor')}
item={{
id: 'x',
name: 'x',
settings: {
baseNameMode,
isClearable: true,
placeholder: 'Use standard color scheme',
},
}}
/>
</Field>
<Field label={'Point size'}>
<ScaleDimensionEditor
value={value.pointSize!}
context={context}
onChange={(field) => onFieldChange(field, 'pointSize')}
item={{
id: 'x',
name: 'x',
settings: {
min: 1,
max: 100,
},
}}
/>
</Field>
</div>
);
};
+2 -2
View File
@@ -17,8 +17,8 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(XYChartPanel2)
defaultValue: 'auto',
settings: {
options: [
{ value: 'auto', label: 'Auto', description: 'No changes to saved model since 8.0' },
{ value: 'manual', label: 'Manual' },
{ value: 'auto', label: 'Table', description: 'Plot values within a single table result' },
{ value: 'manual', label: 'Manual', description: 'Construct values from any result' },
],
},
})
@@ -25,10 +25,11 @@ composableKinds: PanelCfg: {
schemas: [{
version: [0, 0]
schema: {
// Auto is "table" in the UI
SeriesMapping: "auto" | "manual" @cuetsy(kind="enum")
ScatterShow: "points" | "lines" | "points+lines" @cuetsy(kind="enum", memberNames="Points|Lines|PointsAndLines")
// Configuration for the Table/Auto mode
XYDimensionConfig: {
frame: int32 & >=0
x?: string
@@ -66,7 +67,11 @@ composableKinds: PanelCfg: {
common.OptionsWithTooltip
seriesMapping?: SeriesMapping
dims: XYDimensionConfig
// Table Mode (auto)
dims: XYDimensionConfig
// Manual Mode
series: [...ScatterSeriesConfig]
} @cuetsy(kind="interface")
}
@@ -10,6 +10,9 @@
import * as common from '@grafana/schema';
/**
* Auto is "table" in the UI
*/
export enum SeriesMapping {
Auto = 'auto',
Manual = 'manual',
@@ -21,6 +24,9 @@ export enum ScatterShow {
PointsAndLines = 'points+lines',
}
/**
* Configuration for the Table/Auto mode
*/
export interface XYDimensionConfig {
exclude?: Array<string>;
frame: number;
@@ -54,7 +60,13 @@ export interface ScatterSeriesConfig extends FieldConfig {
}
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip {
/**
* Table Mode (auto)
*/
dims: XYDimensionConfig;
/**
* Manual Mode
*/
series: Array<ScatterSeriesConfig>;
seriesMapping?: SeriesMapping;
}