mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
333 lines
11 KiB
TypeScript
333 lines
11 KiB
TypeScript
import { css, cx } from '@emotion/css';
|
|
import React, { Fragment, useState } from 'react';
|
|
import { usePrevious } from 'react-use';
|
|
|
|
import {
|
|
getFrameDisplayName,
|
|
StandardEditorProps,
|
|
// getFieldDisplayName,
|
|
FrameMatcherID,
|
|
FieldMatcherID,
|
|
FieldNamePickerBaseNameMode,
|
|
FieldType,
|
|
GrafanaTheme2,
|
|
} from '@grafana/data';
|
|
import { Button, Field, IconButton, Select, useStyles2 } from '@grafana/ui';
|
|
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
|
|
import { LayerName } from 'app/core/components/Layers/LayerName';
|
|
|
|
import { Options, SeriesMapping, XYSeriesConfig } from './panelcfg.gen';
|
|
|
|
export const SeriesEditor = ({
|
|
value: seriesCfg,
|
|
onChange,
|
|
context,
|
|
}: StandardEditorProps<XYSeriesConfig[], unknown, Options>) => {
|
|
const style = useStyles2(getStyles);
|
|
|
|
// reset opts when mapping changes (no way to do this in panel opts builder?)
|
|
const mapping = context.options?.mapping as SeriesMapping;
|
|
const prevMapping = usePrevious(mapping);
|
|
const mappingChanged = prevMapping != null && mapping !== prevMapping;
|
|
|
|
const defaultFrame = { frame: { matcher: { id: FrameMatcherID.byIndex, options: 0 } } };
|
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
|
|
if (mappingChanged || seriesCfg == null) {
|
|
seriesCfg = [{ ...defaultFrame }];
|
|
onChange([...seriesCfg]);
|
|
|
|
if (selectedIdx > 0) {
|
|
setSelectedIdx(0);
|
|
}
|
|
}
|
|
|
|
const addSeries = () => {
|
|
seriesCfg = seriesCfg.concat({ ...defaultFrame });
|
|
setSelectedIdx(seriesCfg.length - 1);
|
|
onChange([...seriesCfg]);
|
|
};
|
|
|
|
const deleteSeries = (index: number) => {
|
|
seriesCfg = seriesCfg.filter((s, i) => i !== index);
|
|
setSelectedIdx(0);
|
|
onChange([...seriesCfg]);
|
|
};
|
|
|
|
const series = seriesCfg[selectedIdx];
|
|
const formKey = `${mapping}${selectedIdx}`;
|
|
|
|
const baseNameMode =
|
|
mapping === SeriesMapping.Manual
|
|
? FieldNamePickerBaseNameMode.ExcludeBaseNames
|
|
: context.data.length === 1
|
|
? FieldNamePickerBaseNameMode.IncludeAll
|
|
: FieldNamePickerBaseNameMode.OnlyBaseNames;
|
|
|
|
context.data.forEach((frame, frameIndex) => {
|
|
frame.fields.forEach((field, fieldIndex) => {
|
|
field.state = {
|
|
...field.state,
|
|
origin: {
|
|
frameIndex,
|
|
fieldIndex,
|
|
},
|
|
};
|
|
});
|
|
});
|
|
|
|
return (
|
|
<>
|
|
{mapping === SeriesMapping.Manual && (
|
|
<>
|
|
<Button icon="plus" size="sm" variant="secondary" onClick={addSeries} className={style.marginBot}>
|
|
Add series
|
|
</Button>
|
|
|
|
<div className={style.marginBot}>
|
|
{seriesCfg.map((series, index) => {
|
|
return (
|
|
<div
|
|
key={`series/${index}`}
|
|
className={index === selectedIdx ? `${style.row} ${style.sel}` : style.row}
|
|
onClick={() => setSelectedIdx(index)}
|
|
role="button"
|
|
aria-label={`Select series ${index + 1}`}
|
|
tabIndex={0}
|
|
onKeyPress={(e) => {
|
|
if (e.key === 'Enter') {
|
|
setSelectedIdx(index);
|
|
}
|
|
}}
|
|
>
|
|
<LayerName
|
|
name={series.name?.fixed ?? `Series ${index + 1}`}
|
|
onChange={(v) => {
|
|
series.name = {
|
|
fixed: v === '' || v === `Series ${index + 1}` ? undefined : v,
|
|
};
|
|
onChange([...seriesCfg]);
|
|
}}
|
|
/>
|
|
<IconButton
|
|
name="trash-alt"
|
|
title={'remove'}
|
|
className={cx(style.actionIcon)}
|
|
onClick={() => deleteSeries(index)}
|
|
tooltip="Delete series"
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{selectedIdx >= 0 && series != null && (
|
|
<Fragment key={formKey}>
|
|
<Field label="Frame">
|
|
<Select
|
|
placeholder={mapping === SeriesMapping.Auto ? 'All frames' : 'Select frame'}
|
|
isClearable={true}
|
|
options={context.data.map((frame, index) => ({
|
|
value: index,
|
|
label: `${getFrameDisplayName(frame, index)} (index: ${index}, rows: ${frame.length})`,
|
|
}))}
|
|
value={series.frame?.matcher.options}
|
|
onChange={(opt) => {
|
|
if (opt == null) {
|
|
delete series.frame;
|
|
} else {
|
|
series.frame = {
|
|
matcher: {
|
|
id: FrameMatcherID.byIndex,
|
|
options: Number(opt.value),
|
|
},
|
|
};
|
|
}
|
|
|
|
onChange([...seriesCfg]);
|
|
}}
|
|
/>
|
|
</Field>
|
|
<Field label="X field">
|
|
<FieldNamePicker
|
|
value={series.x?.matcher.options as string}
|
|
context={context}
|
|
onChange={(fieldName) => {
|
|
if (fieldName == null) {
|
|
delete series.x;
|
|
} else {
|
|
// TODO: reset any other dim that was set to fieldName
|
|
series.x = {
|
|
matcher: {
|
|
id: FieldMatcherID.byName,
|
|
options: fieldName,
|
|
},
|
|
};
|
|
}
|
|
|
|
onChange([...seriesCfg]);
|
|
}}
|
|
item={{
|
|
id: 'x',
|
|
name: 'x',
|
|
settings: {
|
|
filter: (field) =>
|
|
(mapping === SeriesMapping.Auto ||
|
|
field.state?.origin?.frameIndex === series.frame?.matcher.options) &&
|
|
field.type === FieldType.number &&
|
|
!field.config.custom?.hideFrom?.viz,
|
|
baseNameMode,
|
|
placeholderText: mapping === SeriesMapping.Auto ? 'First number field in each frame' : undefined,
|
|
},
|
|
}}
|
|
/>
|
|
</Field>
|
|
<Field label="Y field">
|
|
<FieldNamePicker
|
|
value={series.y?.matcher?.options as string}
|
|
context={context}
|
|
onChange={(fieldName) => {
|
|
if (fieldName == null) {
|
|
delete series.y;
|
|
} else {
|
|
// TODO: reset any other dim that was set to fieldName
|
|
series.y = {
|
|
matcher: {
|
|
id: FieldMatcherID.byName,
|
|
options: fieldName,
|
|
},
|
|
};
|
|
}
|
|
|
|
onChange([...seriesCfg]);
|
|
}}
|
|
item={{
|
|
id: 'y',
|
|
name: 'y',
|
|
settings: {
|
|
// TODO: filter out series.y?.exclude.options, series.size.matcher.options, series.color.matcher.options
|
|
filter: (field) =>
|
|
(mapping === SeriesMapping.Auto ||
|
|
field.state?.origin?.frameIndex === series.frame?.matcher.options) &&
|
|
field.type === FieldType.number &&
|
|
!field.config.custom?.hideFrom?.viz,
|
|
baseNameMode,
|
|
placeholderText: mapping === SeriesMapping.Auto ? 'Remaining number fields in each frame' : undefined,
|
|
},
|
|
}}
|
|
/>
|
|
</Field>
|
|
<Field label="Size field">
|
|
<FieldNamePicker
|
|
value={series.size?.matcher?.options as string}
|
|
context={context}
|
|
onChange={(fieldName) => {
|
|
if (fieldName == null) {
|
|
delete series.size;
|
|
} else {
|
|
// TODO: reset any other dim that was set to fieldName
|
|
series.size = {
|
|
matcher: {
|
|
id: FieldMatcherID.byName,
|
|
options: fieldName,
|
|
},
|
|
};
|
|
}
|
|
|
|
onChange([...seriesCfg]);
|
|
}}
|
|
item={{
|
|
id: 'size',
|
|
name: 'size',
|
|
settings: {
|
|
// TODO: filter out series.y?.exclude.options, series.size.matcher.options, series.color.matcher.options
|
|
filter: (field) =>
|
|
field.name !== series.x?.matcher.options &&
|
|
(mapping === SeriesMapping.Auto ||
|
|
field.state?.origin?.frameIndex === series.frame?.matcher.options) &&
|
|
field.type === FieldType.number &&
|
|
!field.config.custom?.hideFrom?.viz,
|
|
baseNameMode,
|
|
placeholderText: '',
|
|
},
|
|
}}
|
|
/>
|
|
</Field>
|
|
<Field label="Color field">
|
|
<FieldNamePicker
|
|
value={series.color?.matcher?.options as string}
|
|
context={context}
|
|
onChange={(fieldName) => {
|
|
if (fieldName == null) {
|
|
delete series.color;
|
|
} else {
|
|
// TODO: reset any other dim that was set to fieldName
|
|
series.color = {
|
|
matcher: {
|
|
id: FieldMatcherID.byName,
|
|
options: fieldName,
|
|
},
|
|
};
|
|
}
|
|
|
|
onChange([...seriesCfg]);
|
|
}}
|
|
item={{
|
|
id: 'color',
|
|
name: 'color',
|
|
settings: {
|
|
// TODO: filter out series.y?.exclude.options, series.size.matcher.options, series.color.matcher.options
|
|
filter: (field) =>
|
|
field.name !== series.x?.matcher.options &&
|
|
(mapping === SeriesMapping.Auto ||
|
|
field.state?.origin?.frameIndex === series.frame?.matcher.options) &&
|
|
field.type === FieldType.number &&
|
|
!field.config.custom?.hideFrom?.viz,
|
|
baseNameMode,
|
|
placeholderText: '',
|
|
},
|
|
}}
|
|
/>
|
|
</Field>
|
|
</Fragment>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => ({
|
|
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}`,
|
|
},
|
|
}),
|
|
});
|