New panel edit: data links edit (#22077)
* Move data links suggestions to grafana-data
* Data links - field config and overrides
* Lint
* Fix test
* Add variable suggestions to field override context
* Revert "Move data links suggestions to grafana-data"
This reverts commit 5d8d01a65e.
* Move FieldConfigEditor to core
This commit is contained in:
@@ -52,3 +52,23 @@ export interface LinkModel<T> {
|
||||
export interface LinkModelSupplier<T extends object> {
|
||||
getLinks(scopedVars?: any): Array<LinkModel<T>>;
|
||||
}
|
||||
|
||||
export enum VariableOrigin {
|
||||
Series = 'series',
|
||||
Field = 'field',
|
||||
Fields = 'fields',
|
||||
Value = 'value',
|
||||
BuiltIn = 'built-in',
|
||||
Template = 'template',
|
||||
}
|
||||
|
||||
export interface VariableSuggestion {
|
||||
value: string;
|
||||
label: string;
|
||||
documentation?: string;
|
||||
origin: VariableOrigin;
|
||||
}
|
||||
|
||||
export enum VariableSuggestionsScope {
|
||||
Values = 'values',
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MatcherConfig, FieldConfig, Field } from '../types';
|
||||
import { Registry, RegistryItem } from '../utils';
|
||||
import { ComponentType } from 'react';
|
||||
import { MatcherConfig, FieldConfig, Field, DataFrame, VariableSuggestion, VariableSuggestionsScope } from '../types';
|
||||
import { Registry, RegistryItem } from '../utils';
|
||||
import { InterpolateFunction } from './panel';
|
||||
import { DataFrame } from 'apache-arrow';
|
||||
|
||||
export interface DynamicConfigValue {
|
||||
prop: string;
|
||||
@@ -26,18 +25,20 @@ export interface FieldConfigSource {
|
||||
export interface FieldConfigEditorProps<TValue, TSettings> {
|
||||
item: FieldPropertyEditorItem<TValue, TSettings>; // The property info
|
||||
value: TValue;
|
||||
context: FieldOverrideContext;
|
||||
onChange: (value?: TValue) => void;
|
||||
}
|
||||
|
||||
export interface FieldOverrideContext {
|
||||
field: Field;
|
||||
data: DataFrame;
|
||||
replaceVariables: InterpolateFunction;
|
||||
data: DataFrame[];
|
||||
field?: Field;
|
||||
replaceVariables?: InterpolateFunction;
|
||||
getSuggestions?: (scope?: VariableSuggestionsScope) => VariableSuggestion[];
|
||||
}
|
||||
|
||||
export interface FieldOverrideEditorProps<TValue, TSettings> {
|
||||
item: FieldPropertyEditorItem<TValue, TSettings>;
|
||||
value: any;
|
||||
value: TValue;
|
||||
context: FieldOverrideContext;
|
||||
onChange: (value?: any) => void;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { ChangeEvent, useContext } from 'react';
|
||||
import { DataLink } from '@grafana/data';
|
||||
import { DataLink, VariableSuggestion, GrafanaTheme } from '@grafana/data';
|
||||
import { FormField, Switch } from '../index';
|
||||
import { VariableSuggestion } from './DataLinkSuggestions';
|
||||
import { css } from 'emotion';
|
||||
import { ThemeContext, stylesFactory } from '../../themes/index';
|
||||
import { DataLinkInput } from './DataLinkInput';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
interface DataLinkEditorProps {
|
||||
index: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo, useContext, useRef, RefObject, memo, useEffect } from 'react';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions';
|
||||
import { DataLinkSuggestions } from './DataLinkSuggestions';
|
||||
import { ThemeContext, DataLinkBuiltInVars, makeValue } from '../../index';
|
||||
import { SelectionReference } from './SelectionReference';
|
||||
import { Portal } from '../index';
|
||||
@@ -14,7 +14,7 @@ import { css, cx } from 'emotion';
|
||||
import { SlatePrism } from '../../slate-plugins';
|
||||
import { SCHEMA } from '../../utils/slate';
|
||||
import { stylesFactory } from '../../themes';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { GrafanaTheme, VariableSuggestion, VariableOrigin } from '@grafana/data';
|
||||
|
||||
const modulo = (a: number, n: number) => a - n * Math.floor(a / n);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { selectThemeVariant, ThemeContext } from '../../index';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { GrafanaTheme, VariableSuggestion } from '@grafana/data';
|
||||
import { css, cx } from 'emotion';
|
||||
import _ from 'lodash';
|
||||
import React, { useRef, useContext, useMemo } from 'react';
|
||||
@@ -8,22 +8,6 @@ import { List } from '../index';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
export enum VariableOrigin {
|
||||
Series = 'series',
|
||||
Field = 'field',
|
||||
Fields = 'fields',
|
||||
Value = 'value',
|
||||
BuiltIn = 'built-in',
|
||||
Template = 'template',
|
||||
}
|
||||
|
||||
export interface VariableSuggestion {
|
||||
value: string;
|
||||
label: string;
|
||||
documentation?: string;
|
||||
origin: VariableOrigin;
|
||||
}
|
||||
|
||||
interface DataLinkSuggestionsProps {
|
||||
suggestions: VariableSuggestion[];
|
||||
activeIndex: number;
|
||||
|
||||
@@ -4,10 +4,10 @@ import React, { FC } from 'react';
|
||||
import Prism from 'prismjs';
|
||||
// Components
|
||||
import { css } from 'emotion';
|
||||
import { DataLink } from '@grafana/data';
|
||||
import { DataLink, VariableSuggestion } from '@grafana/data';
|
||||
import { Button } from '../index';
|
||||
import { DataLinkEditor } from './DataLinkEditor';
|
||||
import { VariableSuggestion } from './DataLinkSuggestions';
|
||||
|
||||
import { useTheme } from '../../themes/ThemeContext';
|
||||
|
||||
interface DataLinksEditorProps {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import FieldConfigEditor from './FieldConfigEditor';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { FieldConfigSource, FieldConfigEditorRegistry, FieldPropertyEditorItem, Registry } from '@grafana/data';
|
||||
import { NumberFieldConfigSettings, NumberValueEditor, NumberOverrideEditor, numberOverrideProcessor } from './number';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
|
||||
const FieldConfigStories = storiesOf('UI/FieldConfig', module);
|
||||
|
||||
FieldConfigStories.addDecorator(withCenteredStory);
|
||||
|
||||
const cfg: FieldConfigSource = {
|
||||
defaults: {
|
||||
title: 'Hello',
|
||||
decimals: 3,
|
||||
},
|
||||
overrides: [],
|
||||
};
|
||||
|
||||
const columWidth: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
|
||||
id: 'width', // Match field properties
|
||||
name: 'Column Width',
|
||||
description: 'column width (for table)',
|
||||
|
||||
editor: NumberValueEditor,
|
||||
override: NumberOverrideEditor,
|
||||
process: numberOverrideProcessor,
|
||||
|
||||
settings: {
|
||||
placeholder: 'auto',
|
||||
min: 20,
|
||||
max: 300,
|
||||
},
|
||||
};
|
||||
|
||||
export const customEditorRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(() => {
|
||||
return [columWidth];
|
||||
});
|
||||
|
||||
FieldConfigStories.add('default', () => {
|
||||
return renderComponentWithTheme(FieldConfigEditor, {
|
||||
config: cfg,
|
||||
data: [],
|
||||
custom: customEditorRegistry,
|
||||
onChange: (config: FieldConfigSource) => {
|
||||
console.log('Data', config);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,239 +0,0 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
import { ControlledCollapse } from '../Collapse/Collapse';
|
||||
import { ValuePicker } from '../ValuePicker/ValuePicker';
|
||||
|
||||
interface Props {
|
||||
config: FieldConfigSource;
|
||||
custom?: FieldConfigEditorRegistry; // custom fields
|
||||
include?: string[]; // Ordered list of which fields should be shown/included
|
||||
onChange: (config: FieldConfigSource) => void;
|
||||
|
||||
// Helpful for IntelliSense
|
||||
data: DataFrame[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects the container div to have size set and will fill it 100%
|
||||
*/
|
||||
export class FieldConfigEditor extends React.PureComponent<Props> {
|
||||
private setDefaultValue = (name: string, value: any, custom: boolean) => {
|
||||
const defaults = { ...this.props.config.defaults };
|
||||
const remove = value === undefined || value === null || '';
|
||||
if (custom) {
|
||||
if (defaults.custom) {
|
||||
if (remove) {
|
||||
defaults.custom = { ...defaults.custom };
|
||||
delete defaults.custom[name];
|
||||
} else {
|
||||
defaults.custom = { ...defaults.custom, [name]: value };
|
||||
}
|
||||
} else if (!remove) {
|
||||
defaults.custom = { [name]: value };
|
||||
}
|
||||
} else if (remove) {
|
||||
delete (defaults as any)[name];
|
||||
} else {
|
||||
(defaults as any)[name] = value;
|
||||
}
|
||||
|
||||
this.props.onChange({
|
||||
...this.props.config,
|
||||
defaults,
|
||||
});
|
||||
};
|
||||
|
||||
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];
|
||||
|
||||
return (
|
||||
<Forms.Field label={item.name} description={item.description} key={`${item.id}/${custom}`}>
|
||||
<item.editor item={item} value={value} onChange={v => this.setDefaultValue(item.id, v, custom)} />
|
||||
</Forms.Field>
|
||||
);
|
||||
}
|
||||
|
||||
renderStandardConfigs() {
|
||||
const { include } = this.props;
|
||||
if (include) {
|
||||
return include.map(f => this.renderEditor(standardFieldConfigEditorRegistry.get(f), false));
|
||||
}
|
||||
return standardFieldConfigEditorRegistry.list().map(f => this.renderEditor(f, false));
|
||||
}
|
||||
|
||||
renderCustomConfigs() {
|
||||
const { custom } = this.props;
|
||||
if (!custom) {
|
||||
return null;
|
||||
}
|
||||
return custom.list().map(f => this.renderEditor(f, true));
|
||||
}
|
||||
|
||||
renderOverrides() {
|
||||
const { config, data, custom } = this.props;
|
||||
if (config.overrides.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div>
|
||||
{config.overrides.map((o, i) => {
|
||||
const matcherUi = fieldMatchersUI.get(o.matcher.id);
|
||||
return (
|
||||
<div key={`${o.matcher.id}/${i}`} style={{ border: `2px solid red`, marginBottom: '10px' }}>
|
||||
<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)}
|
||||
/>
|
||||
|
||||
<div style={{ border: `2px solid blue`, marginBottom: '5px' }}>
|
||||
{o.properties.map((p, j) => {
|
||||
const reg = p.custom ? custom : standardFieldConfigEditorRegistry;
|
||||
const item = reg?.getIfExists(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>
|
||||
);
|
||||
})}
|
||||
<ValuePicker
|
||||
icon="plus"
|
||||
label="Set config property"
|
||||
options={configPropertiesOptions}
|
||||
onChange={o => {
|
||||
this.onDynamicConfigValueAdd(i, o.value!, o.custom);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</Forms.Field>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAddOverride = () => {
|
||||
return (
|
||||
<ValuePicker
|
||||
icon="plus"
|
||||
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>
|
||||
<ControlledCollapse label="Standard Field Configuration" collapsible>
|
||||
{this.renderStandardConfigs()}
|
||||
</ControlledCollapse>
|
||||
{this.props.custom && (
|
||||
<ControlledCollapse label="Standard Field Configuration">{this.renderCustomConfigs()}</ControlledCollapse>
|
||||
)}
|
||||
|
||||
<ControlledCollapse label="Field Overrides" collapsible>
|
||||
{this.renderOverrides()}
|
||||
{this.renderAddOverride()}
|
||||
</ControlledCollapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldConfigEditor;
|
||||
@@ -0,0 +1,225 @@
|
||||
import {
|
||||
FieldOverrideContext,
|
||||
FieldConfigEditorProps,
|
||||
DataLink,
|
||||
FieldOverrideEditorProps,
|
||||
DataFrame,
|
||||
} from '@grafana/data';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import Forms from '../Forms';
|
||||
import { Modal } from '../Modal/Modal';
|
||||
import { DataLinkEditor } from '../DataLinks/DataLinkEditor';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { VariableSuggestion } from '@grafana/data';
|
||||
|
||||
export interface DataLinksFieldConfigSettings {}
|
||||
|
||||
export const dataLinksOverrideProcessor = (
|
||||
value: any,
|
||||
context: FieldOverrideContext,
|
||||
_settings: DataLinksFieldConfigSettings
|
||||
) => {
|
||||
return value as DataLink[];
|
||||
};
|
||||
|
||||
export const DataLinksValueEditor: React.FC<FieldConfigEditorProps<DataLink[], DataLinksFieldConfigSettings>> = ({
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
}) => {
|
||||
const onDataLinkChange = (index: number, link: DataLink) => {
|
||||
const links = cloneDeep(value);
|
||||
links[index] = link;
|
||||
onChange(links);
|
||||
};
|
||||
|
||||
const onDataLinkAdd = () => {
|
||||
const links = cloneDeep(value);
|
||||
|
||||
links.push({
|
||||
title: '',
|
||||
url: '',
|
||||
});
|
||||
onChange(links);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{value.map((l, i) => {
|
||||
return (
|
||||
<DataLinksListItem
|
||||
key={`${l.title}/${i}`}
|
||||
index={i}
|
||||
link={l}
|
||||
onChange={onDataLinkChange}
|
||||
data={context.data}
|
||||
suggestions={context.getSuggestions ? context.getSuggestions() : []}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Forms.Button size="sm" icon="fa fa-plus" onClick={onDataLinkAdd}>
|
||||
Create data link
|
||||
</Forms.Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DataLinksOverrideEditor: React.FC<FieldOverrideEditorProps<DataLink[], DataLinksFieldConfigSettings>> = ({
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
item,
|
||||
}) => {
|
||||
const onDataLinkChange = (index: number, link: DataLink) => {
|
||||
const links = cloneDeep(value);
|
||||
links[index] = link;
|
||||
onChange(links);
|
||||
};
|
||||
|
||||
const onDataLinkAdd = () => {
|
||||
let links = cloneDeep(value);
|
||||
if (links) {
|
||||
links.push({
|
||||
title: '',
|
||||
url: '',
|
||||
});
|
||||
} else {
|
||||
links = [
|
||||
{
|
||||
title: '',
|
||||
url: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
onChange(links);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{value &&
|
||||
value.map((l, i) => {
|
||||
return (
|
||||
<DataLinksListItem
|
||||
key={`${l.title}/${i}`}
|
||||
index={i}
|
||||
link={l}
|
||||
onChange={onDataLinkChange}
|
||||
data={context.data}
|
||||
suggestions={context.getSuggestions ? context.getSuggestions() : []}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Forms.Button size="sm" icon="fa fa-plus" onClick={onDataLinkAdd}>
|
||||
Create data link
|
||||
</Forms.Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DataLinksListItemProps {
|
||||
index: number;
|
||||
link: DataLink;
|
||||
data: DataFrame[];
|
||||
onChange: (index: number, link: DataLink) => void;
|
||||
suggestions: VariableSuggestion[];
|
||||
}
|
||||
|
||||
const DataLinksListItem: FC<DataLinksListItemProps> = ({ index, link, data, onChange, suggestions }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const style = () => {
|
||||
return {
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`,
|
||||
action: css`
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
`,
|
||||
noTitle: css`
|
||||
font-style: italic;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
const styles = style();
|
||||
const hasTitle = link.title.trim() !== '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={cx(!hasTitle && styles.noTitle)}>{hasTitle ? link.title : 'Edit data link'}</div>
|
||||
<div>
|
||||
<Forms.Button size="sm" icon="fa fa-pencil" variant="link" onClick={() => setIsEditing(true)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<Modal
|
||||
title="Edit data link"
|
||||
isOpen={isEditing}
|
||||
onDismiss={() => {
|
||||
setIsEditing(false);
|
||||
}}
|
||||
>
|
||||
<DataLinkEditorModalContent
|
||||
index={index}
|
||||
link={link}
|
||||
data={data}
|
||||
onChange={onChange}
|
||||
onClose={() => setIsEditing(false)}
|
||||
suggestions={suggestions}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DataLinkEditorModalContentProps {
|
||||
link: DataLink;
|
||||
index: number;
|
||||
data: DataFrame[];
|
||||
suggestions: VariableSuggestion[];
|
||||
onChange: (index: number, ink: DataLink) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DataLinkEditorModalContent: FC<DataLinkEditorModalContentProps> = ({
|
||||
link,
|
||||
index,
|
||||
data,
|
||||
suggestions,
|
||||
onChange,
|
||||
onClose,
|
||||
}) => {
|
||||
const [dirtyLink, setDirtyLink] = useState(link);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataLinkEditor
|
||||
value={dirtyLink}
|
||||
index={index}
|
||||
isLast={false}
|
||||
suggestions={suggestions}
|
||||
onChange={(index, link) => {
|
||||
setDirtyLink(link);
|
||||
}}
|
||||
onRemove={() => {}}
|
||||
/>
|
||||
<Forms.Button
|
||||
onClick={() => {
|
||||
onChange(index, dirtyLink);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Forms.Button>
|
||||
<Forms.Button onClick={() => onClose()}>Cancel</Forms.Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -10,6 +10,7 @@ describe('standardFieldConfigEditorRegistry', () => {
|
||||
thresholds: {} as any,
|
||||
noValue: 'no value',
|
||||
unit: 'km/s',
|
||||
links: {} as any,
|
||||
};
|
||||
|
||||
it('make sure all fields have a valid name', () => {
|
||||
|
||||
+21
-2
@@ -1,4 +1,10 @@
|
||||
import { FieldConfigEditorRegistry, Registry, FieldPropertyEditorItem, ThresholdsConfig } from '@grafana/data';
|
||||
import {
|
||||
FieldConfigEditorRegistry,
|
||||
Registry,
|
||||
FieldPropertyEditorItem,
|
||||
ThresholdsConfig,
|
||||
DataLink,
|
||||
} from '@grafana/data';
|
||||
import { StringValueEditor, StringOverrideEditor, stringOverrideProcessor, StringFieldConfigSettings } from './string';
|
||||
import { NumberValueEditor, NumberOverrideEditor, numberOverrideProcessor, NumberFieldConfigSettings } from './number';
|
||||
import { UnitValueEditor, UnitOverrideEditor } from './units';
|
||||
@@ -8,6 +14,7 @@ import {
|
||||
thresholdsOverrideProcessor,
|
||||
ThresholdsFieldConfigSettings,
|
||||
} from './thresholds';
|
||||
import { DataLinksValueEditor, DataLinksOverrideEditor, dataLinksOverrideProcessor } from './links';
|
||||
|
||||
const title: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
|
||||
id: 'title', // Match field properties
|
||||
@@ -110,8 +117,20 @@ const noValue: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
|
||||
},
|
||||
};
|
||||
|
||||
const links: FieldPropertyEditorItem<DataLink[], StringFieldConfigSettings> = {
|
||||
id: 'links', // Match field properties
|
||||
name: 'DataLinks',
|
||||
description: 'Manage date links',
|
||||
editor: DataLinksValueEditor,
|
||||
override: DataLinksOverrideEditor,
|
||||
process: dataLinksOverrideProcessor,
|
||||
settings: {
|
||||
placeholder: '-',
|
||||
},
|
||||
};
|
||||
|
||||
export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(
|
||||
() => {
|
||||
return [title, unit, min, max, decimals, thresholds, noValue];
|
||||
return [title, unit, min, max, decimals, thresholds, noValue, links];
|
||||
}
|
||||
);
|
||||
|
||||
@@ -95,7 +95,6 @@ export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
|
||||
export * from './SingleStatShared/index';
|
||||
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
||||
export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu';
|
||||
export { VariableSuggestion, VariableOrigin } from './DataLinks/DataLinkSuggestions';
|
||||
export { DataLinksEditor } from './DataLinks/DataLinksEditor';
|
||||
export { DataLinkInput } from './DataLinks/DataLinkInput';
|
||||
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
|
||||
@@ -118,7 +117,6 @@ export { Icon } from './Icon/Icon';
|
||||
export { Drawer } from './Drawer/Drawer';
|
||||
|
||||
// TODO: namespace!!
|
||||
export { FieldConfigEditor } from './FieldConfigs/FieldConfigEditor';
|
||||
export {
|
||||
StringValueEditor,
|
||||
StringOverrideEditor,
|
||||
@@ -135,3 +133,5 @@ export {
|
||||
// Next-gen forms
|
||||
export { default as Forms } from './Forms';
|
||||
export { ValuePicker } from './ValuePicker/ValuePicker';
|
||||
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';
|
||||
export { standardFieldConfigEditorRegistry } from './FieldConfigs/standardFieldConfigEditorRegistry';
|
||||
|
||||
Reference in New Issue
Block a user