mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
New panel edit: field overrides ui (#22036)
* Add title editor * Wip * FIeld config overrides UI (v1) * Basic property override editors * name to prop * use prop not path Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
25431f32f0
commit
1728742096
@ -24,8 +24,8 @@ describe('FieldOverrides', () => {
|
||||
{
|
||||
matcher: { id: FieldMatcherID.numeric },
|
||||
properties: [
|
||||
{ path: 'decimals', value: 1 }, // Numeric
|
||||
{ path: 'title', value: 'Kittens' }, // Text
|
||||
{ prop: 'decimals', value: 1 }, // Numeric
|
||||
{ prop: 'title', value: 'Kittens' }, // Text
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -218,8 +218,8 @@ function prepareConfigValue(key: string, input: any, options?: DynamicConfigValu
|
||||
|
||||
export function setDynamicConfigValue(config: FieldConfig, options: DynamicConfigValueOptions) {
|
||||
const { value } = options;
|
||||
const v = prepareConfigValue(value.path, value.value, options);
|
||||
set(config, value.path, v);
|
||||
const v = prepareConfigValue(value.prop, value.value, options);
|
||||
set(config, value.prop, v);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,8 +5,9 @@ import { InterpolateFunction } from './panel';
|
||||
import { DataFrame } from 'apache-arrow';
|
||||
|
||||
export interface DynamicConfigValue {
|
||||
path: string;
|
||||
value: any;
|
||||
prop: string;
|
||||
value?: any;
|
||||
custom?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigOverrideRule {
|
||||
|
@ -1,7 +1,15 @@
|
||||
import React from 'react';
|
||||
import { FieldConfigEditorRegistry, FieldConfigSource, DataFrame, FieldPropertyEditorItem } from '@grafana/data';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {
|
||||
FieldConfigEditorRegistry,
|
||||
FieldConfigSource,
|
||||
DataFrame,
|
||||
FieldPropertyEditorItem,
|
||||
DynamicConfigValue,
|
||||
} from '@grafana/data';
|
||||
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
|
||||
import Forms from '../Forms';
|
||||
import { fieldMatchersUI } from '../MatchersUI/fieldMatchersUI';
|
||||
|
||||
interface Props {
|
||||
config: FieldConfigSource;
|
||||
@ -43,6 +51,41 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
|
||||
});
|
||||
};
|
||||
|
||||
onMatcherConfigChange = (index: number, matcherConfig?: any) => {
|
||||
const { config } = this.props;
|
||||
let overrides = cloneDeep(config.overrides);
|
||||
if (matcherConfig === undefined) {
|
||||
overrides = overrides.splice(index, 1);
|
||||
} else {
|
||||
overrides[index].matcher.options = matcherConfig;
|
||||
}
|
||||
this.props.onChange({ ...config, overrides });
|
||||
};
|
||||
|
||||
onDynamicConfigValueAdd = (index: number, prop: string, custom?: boolean) => {
|
||||
const { config } = this.props;
|
||||
let overrides = cloneDeep(config.overrides);
|
||||
|
||||
const propertyConfig: DynamicConfigValue = {
|
||||
prop,
|
||||
custom,
|
||||
};
|
||||
if (overrides[index].properties) {
|
||||
overrides[index].properties.push(propertyConfig);
|
||||
} else {
|
||||
overrides[index].properties = [propertyConfig];
|
||||
}
|
||||
|
||||
this.props.onChange({ ...config, overrides });
|
||||
};
|
||||
|
||||
onDynamicConfigValueChange = (overrideIndex: number, propertyIndex: number, value?: any) => {
|
||||
const { config } = this.props;
|
||||
let overrides = cloneDeep(config.overrides);
|
||||
overrides[overrideIndex].properties[propertyIndex].value = value;
|
||||
this.props.onChange({ ...config, overrides });
|
||||
};
|
||||
|
||||
renderEditor(item: FieldPropertyEditorItem, custom: boolean) {
|
||||
const config = this.props.config.defaults;
|
||||
const value = custom ? (config.custom ? config.custom[item.id] : undefined) : (config as any)[item.id];
|
||||
@ -71,20 +114,110 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
|
||||
}
|
||||
|
||||
renderOverrides() {
|
||||
return <div>Override rules</div>;
|
||||
const { config, data, custom } = this.props;
|
||||
|
||||
let configPropertiesOptions = standardFieldConfigEditorRegistry.list().map(i => ({
|
||||
label: i.name,
|
||||
value: i.id,
|
||||
description: i.description,
|
||||
custom: false,
|
||||
}));
|
||||
|
||||
if (custom) {
|
||||
configPropertiesOptions = configPropertiesOptions.concat(
|
||||
custom.list().map(i => ({
|
||||
label: i.name,
|
||||
value: i.id,
|
||||
description: i.description,
|
||||
custom: true,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{config.overrides.map((o, i) => {
|
||||
const matcherUi = fieldMatchersUI.get(o.matcher.id);
|
||||
return (
|
||||
<div key={`${o.matcher.id}/${i}`}>
|
||||
<Forms.Field label={matcherUi.name} description={matcherUi.description}>
|
||||
<>
|
||||
<matcherUi.component
|
||||
matcher={matcherUi.matcher}
|
||||
data={data}
|
||||
options={o.matcher.options}
|
||||
onChange={option => this.onMatcherConfigChange(i, option)}
|
||||
/>
|
||||
<Forms.ButtonSelect
|
||||
icon="plus"
|
||||
placeholder="Set config property"
|
||||
options={configPropertiesOptions}
|
||||
onChange={o => {
|
||||
this.onDynamicConfigValueAdd(i, o.value!, o.custom);
|
||||
}}
|
||||
/>
|
||||
|
||||
{o.properties.map((p, j) => {
|
||||
const reg = p.custom ? custom : standardFieldConfigEditorRegistry;
|
||||
const item = reg?.get(p.prop);
|
||||
if (!item) {
|
||||
return <div>Unknown property: {p.prop}</div>;
|
||||
}
|
||||
return (
|
||||
<Forms.Field label={item.name} description={item.description}>
|
||||
<item.override
|
||||
value={p.value}
|
||||
onChange={value => {
|
||||
this.onDynamicConfigValueChange(i, j, value);
|
||||
}}
|
||||
item={item}
|
||||
context={{} as any}
|
||||
/>
|
||||
</Forms.Field>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</Forms.Field>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderAddOverride() {
|
||||
return <div>Override rules</div>;
|
||||
}
|
||||
renderAddOverride = () => {
|
||||
return (
|
||||
<Forms.ButtonSelect
|
||||
icon="plus"
|
||||
placeholder={'Add override'}
|
||||
value={{ label: 'Add override' }}
|
||||
options={fieldMatchersUI.list().map(i => ({ label: i.name, value: i.id, description: i.description }))}
|
||||
onChange={value => {
|
||||
const { onChange, config } = this.props;
|
||||
onChange({
|
||||
...config,
|
||||
overrides: [
|
||||
...config.overrides,
|
||||
{
|
||||
matcher: {
|
||||
id: value.value!,
|
||||
},
|
||||
properties: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderStandardConfigs()}
|
||||
{this.renderCustomConfigs()}
|
||||
{this.renderOverrides()}
|
||||
{this.renderAddOverride()}
|
||||
{this.renderOverrides()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FieldOverrideContext, FieldOverrideEditorProps, FieldConfigEditorProps } from '@grafana/data';
|
||||
import {
|
||||
FieldOverrideContext,
|
||||
FieldOverrideEditorProps,
|
||||
FieldConfigEditorProps,
|
||||
toIntegerOrUndefined,
|
||||
toFloatOrUndefined,
|
||||
} from '@grafana/data';
|
||||
import Forms from '../Forms';
|
||||
|
||||
export interface NumberFieldConfigSettings {
|
||||
@ -32,27 +38,37 @@ export const NumberValueEditor: React.FC<FieldConfigEditorProps<number, NumberFi
|
||||
return (
|
||||
<Forms.Input
|
||||
value={isNaN(value) ? '' : value}
|
||||
min={settings.min}
|
||||
max={settings.max}
|
||||
type="number"
|
||||
step={settings.step}
|
||||
onChange={e => {
|
||||
onChange(
|
||||
item.settings.integer
|
||||
? parseInt(e.currentTarget.value, settings.step || 10)
|
||||
: parseFloat(e.currentTarget.value)
|
||||
settings.integer ? toIntegerOrUndefined(e.currentTarget.value) : toFloatOrUndefined(e.currentTarget.value)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export class NumberOverrideEditor extends React.PureComponent<
|
||||
FieldOverrideEditorProps<number, NumberFieldConfigSettings>
|
||||
> {
|
||||
constructor(props: FieldOverrideEditorProps<number, NumberFieldConfigSettings>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>SHOW OVERRIDE EDITOR {this.props.item.name}</div>;
|
||||
}
|
||||
}
|
||||
export const NumberOverrideEditor: React.FC<FieldOverrideEditorProps<number, NumberFieldConfigSettings>> = ({
|
||||
value,
|
||||
onChange,
|
||||
item,
|
||||
}) => {
|
||||
const { settings } = item;
|
||||
return (
|
||||
<Forms.Input
|
||||
value={isNaN(value) ? '' : value}
|
||||
min={settings.min}
|
||||
max={settings.max}
|
||||
type="number"
|
||||
step={settings.step}
|
||||
onChange={e => {
|
||||
onChange(
|
||||
settings.integer ? toIntegerOrUndefined(e.currentTarget.value) : toFloatOrUndefined(e.currentTarget.value)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -23,14 +23,9 @@ export const StringValueEditor: React.FC<FieldConfigEditorProps<string, StringFi
|
||||
return <Forms.Input value={value || ''} onChange={e => onChange(e.currentTarget.value)} />;
|
||||
};
|
||||
|
||||
export class StringOverrideEditor extends React.PureComponent<
|
||||
FieldOverrideEditorProps<string, StringFieldConfigSettings>
|
||||
> {
|
||||
constructor(props: FieldOverrideEditorProps<string, StringFieldConfigSettings>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>SHOW OVERRIDE EDITOR {this.props.item.name}</div>;
|
||||
}
|
||||
}
|
||||
export const StringOverrideEditor: React.FC<FieldOverrideEditorProps<string, StringFieldConfigSettings>> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
return <Forms.Input value={value || ''} onChange={e => onChange(e.currentTarget.value)} />;
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import { getFormStyles } from './getFormStyles';
|
||||
import { Label } from './Label';
|
||||
import { Input } from './Input/Input';
|
||||
import { Select } from './Select/Select';
|
||||
import { ButtonSelect } from './Select/ButtonSelect';
|
||||
import { Form } from './Form';
|
||||
import { Field } from './Field';
|
||||
import { Button, LinkButton } from './Button';
|
||||
@ -16,6 +17,7 @@ const Forms = {
|
||||
Button,
|
||||
LinkButton,
|
||||
Select,
|
||||
ButtonSelect,
|
||||
InputControl,
|
||||
};
|
||||
|
||||
|
@ -1,12 +1,35 @@
|
||||
import React from 'react';
|
||||
import { MatcherUIProps, FieldMatcherUIRegistryItem } from './types';
|
||||
import { FieldMatcherID, fieldMatchers } from '@grafana/data';
|
||||
import Forms from '../Forms';
|
||||
|
||||
export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<string>> {
|
||||
render() {
|
||||
const { matcher } = this.props;
|
||||
const { data, options, onChange } = this.props;
|
||||
const names: Set<string> = new Set();
|
||||
|
||||
return <div>TODO: MATCH STRING for: {matcher.id}</div>;
|
||||
for (const frame of data) {
|
||||
for (const field of frame.fields) {
|
||||
names.add(field.name);
|
||||
}
|
||||
}
|
||||
if (options) {
|
||||
names.add(options);
|
||||
}
|
||||
const selectOptions = Array.from(names).map(n => ({
|
||||
value: n,
|
||||
label: n,
|
||||
}));
|
||||
const selectedOption = selectOptions.find(v => v.value === options);
|
||||
|
||||
return (
|
||||
<Forms.Select
|
||||
allowCustomValue
|
||||
value={selectedOption}
|
||||
options={selectOptions}
|
||||
onChange={o => onChange(o.value!)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,7 +240,11 @@ export class PanelEditor extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
if (plugin.editor && panel) {
|
||||
return <plugin.editor data={data} options={panel.getOptions()} onOptionsChange={this.onPanelOptionsChanged} />;
|
||||
return (
|
||||
<div style={{ marginTop: '40px' }}>
|
||||
<plugin.editor data={data} options={panel.getOptions()} onOptionsChange={this.onPanelOptionsChanged} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div>No editor (angular?)</div>;
|
||||
|
Loading…
Reference in New Issue
Block a user