mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -0,0 +1,255 @@
|
||||
import React from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {
|
||||
FieldConfigEditorRegistry,
|
||||
FieldConfigSource,
|
||||
DataFrame,
|
||||
FieldPropertyEditorItem,
|
||||
DynamicConfigValue,
|
||||
VariableSuggestionsScope,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
standardFieldConfigEditorRegistry,
|
||||
Forms,
|
||||
fieldMatchersUI,
|
||||
ControlledCollapse,
|
||||
ValuePicker,
|
||||
} from '@grafana/ui';
|
||||
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
|
||||
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 { data } = this.props;
|
||||
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)}
|
||||
context={{
|
||||
data,
|
||||
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
|
||||
}}
|
||||
/>
|
||||
</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={{
|
||||
data,
|
||||
getSuggestions: (scope?: VariableSuggestionsScope) =>
|
||||
getDataLinksVariableSuggestions(data, scope),
|
||||
}}
|
||||
/>
|
||||
</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;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { PureComponent, CSSProperties } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
GrafanaTheme,
|
||||
FieldConfigSource,
|
||||
@@ -9,18 +9,10 @@ import {
|
||||
SelectableValue,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
stylesFactory,
|
||||
Forms,
|
||||
FieldConfigEditor,
|
||||
CustomScrollbar,
|
||||
selectThemeVariant,
|
||||
ControlledCollapse,
|
||||
} from '@grafana/ui';
|
||||
import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant, ControlledCollapse } from '@grafana/ui';
|
||||
import { css, cx } from 'emotion';
|
||||
import config from 'app/core/config';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
@@ -36,6 +28,8 @@ import { DisplayMode, displayModes } from './types';
|
||||
import { PanelEditorTabs } from './PanelEditorTabs';
|
||||
import { DashNavTimeControls } from '../DashNav/DashNavTimeControls';
|
||||
import { LocationState, CoreEvents } from 'app/types';
|
||||
import { calculatePanelSize } from './utils';
|
||||
import { FieldConfigEditor } from './FieldConfigEditor';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const handleColor = selectThemeVariant(
|
||||
@@ -407,28 +401,6 @@ export class PanelEditor extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
function calculatePanelSize(mode: DisplayMode, width: number, height: number, panel: PanelModel): CSSProperties {
|
||||
if (mode === DisplayMode.Fill) {
|
||||
return { width, height };
|
||||
}
|
||||
const colWidth = (window.innerWidth - GRID_CELL_VMARGIN * 4) / GRID_COLUMN_COUNT;
|
||||
const pWidth = colWidth * panel.gridPos.w;
|
||||
const pHeight = GRID_CELL_HEIGHT * panel.gridPos.h;
|
||||
const scale = Math.min(width / pWidth, height / pHeight);
|
||||
|
||||
if (mode === DisplayMode.Exact && pWidth <= width && pHeight <= height) {
|
||||
return {
|
||||
width: pWidth,
|
||||
height: pHeight,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: pWidth * scale,
|
||||
height: pHeight * scale,
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
location: state.location,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CSSProperties } from 'react';
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
import { GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, GRID_CELL_HEIGHT } from 'app/core/constants';
|
||||
import { DisplayMode } from './types';
|
||||
|
||||
export function calculatePanelSize(mode: DisplayMode, width: number, height: number, panel: PanelModel): CSSProperties {
|
||||
if (mode === DisplayMode.Fill) {
|
||||
return { width, height };
|
||||
}
|
||||
const colWidth = (window.innerWidth - GRID_CELL_VMARGIN * 4) / GRID_COLUMN_COUNT;
|
||||
const pWidth = colWidth * panel.gridPos.w;
|
||||
const pHeight = GRID_CELL_HEIGHT * panel.gridPos.h;
|
||||
const scale = Math.min(width / pWidth, height / pHeight);
|
||||
|
||||
if (mode === DisplayMode.Exact && pWidth <= width && pHeight <= height) {
|
||||
return {
|
||||
width: pWidth,
|
||||
height: pHeight,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: pWidth * scale,
|
||||
height: pHeight * scale,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user