Commit e612d7a2 by Dominik Prokop Committed by GitHub

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 5d8d01a65eeda8db2379fddfc1223a9ec9a00c1d.

* Move FieldConfigEditor to core
parent 94b66258
......@@ -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',
}
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;
}
......
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;
......
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);
......
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 {
......
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);
},
});
});
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>
</>
);
};
......@@ -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', () => {
......
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';
......@@ -6,13 +6,16 @@ import {
DataFrame,
FieldPropertyEditorItem,
DynamicConfigValue,
VariableSuggestionsScope,
} 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';
import {
standardFieldConfigEditorRegistry,
Forms,
fieldMatchersUI,
ControlledCollapse,
ValuePicker,
} from '@grafana/ui';
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
interface Props {
config: FieldConfigSource;
custom?: FieldConfigEditorRegistry; // custom fields
......@@ -89,12 +92,21 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
};
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)} />
<item.editor
item={item}
value={value}
onChange={v => this.setDefaultValue(item.id, v, custom)}
context={{
data,
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
}}
/>
</Forms.Field>
);
}
......@@ -169,7 +181,11 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
this.onDynamicConfigValueChange(i, j, value);
}}
item={item}
context={{} as any}
context={{
data,
getSuggestions: (scope?: VariableSuggestionsScope) =>
getDataLinksVariableSuggestions(data, scope),
}}
/>
</Forms.Field>
);
......
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,
});
......
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,
};
}
......@@ -6,7 +6,7 @@ import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
import { sanitizeUrl } from 'app/core/utils/text';
import { getConfig } from 'app/core/config';
import locationUtil from 'app/core/utils/location_util';
import { VariableSuggestion, VariableOrigin, DataLinkBuiltInVars } from '@grafana/ui';
import { DataLinkBuiltInVars } from '@grafana/ui';
import {
DataLink,
KeyValue,
......@@ -16,6 +16,9 @@ import {
ScopedVars,
FieldType,
Field,
VariableSuggestion,
VariableOrigin,
VariableSuggestionsScope,
} from '@grafana/data';
const timeRangeVars = [
......@@ -180,21 +183,33 @@ const getDataFrameVars = (dataFrames: DataFrame[]) => {
return suggestions;
};
export const getDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => {
export const getDataLinksVariableSuggestions = (
dataFrames: DataFrame[],
scope?: VariableSuggestionsScope
): VariableSuggestion[] => {
const valueTimeVar = {
value: `${DataLinkBuiltInVars.valueTime}`,
label: 'Time',
documentation: 'Time value of the clicked datapoint (in ms epoch)',
origin: VariableOrigin.Value,
};
return [
...seriesVars,
...getFieldVars(dataFrames),
...valueVars,
valueTimeVar,
...getDataFrameVars(dataFrames),
...getPanelLinksVariableSuggestions(),
];
const includeValueVars = scope === VariableSuggestionsScope.Values;
return includeValueVars
? [
...seriesVars,
...getFieldVars(dataFrames),
...valueVars,
valueTimeVar,
...getDataFrameVars(dataFrames),
...getPanelLinksVariableSuggestions(),
]
: [
...seriesVars,
...getFieldVars(dataFrames),
...getDataFrameVars(dataFrames),
...getPanelLinksVariableSuggestions(),
];
};
export const getCalculationValueDataLinksVariableSuggestions = (dataFrames: DataFrame[]): VariableSuggestion[] => {
......
import React from 'react';
import { css } from 'emotion';
import { Button, FormField, VariableSuggestion, DataLinkInput, stylesFactory } from '@grafana/ui';
import { VariableSuggestion } from '@grafana/data';
import { Button, FormField, DataLinkInput, stylesFactory } from '@grafana/ui';
import { DataLinkConfig } from '../types';
const getStyles = stylesFactory(() => ({
......
import React from 'react';
import { css } from 'emotion';
import { Button, DataLinkBuiltInVars, stylesFactory, useTheme, VariableOrigin } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { Button, DataLinkBuiltInVars, stylesFactory, useTheme } from '@grafana/ui';
import { GrafanaTheme, VariableOrigin } from '@grafana/data';
import { DataLinkConfig } from '../types';
import { DataLink } from './DataLink';
......
import React from 'react';
import { css } from 'emotion';
import { Button, FormField, VariableSuggestion, DataLinkInput, stylesFactory } from '@grafana/ui';
import { Button, FormField, DataLinkInput, stylesFactory } from '@grafana/ui';
import { VariableSuggestion } from '@grafana/data';
import { DerivedFieldConfig } from '../types';
const getStyles = stylesFactory(() => ({
......
import React, { useState } from 'react';
import { css } from 'emotion';
import { Button, DataLinkBuiltInVars, stylesFactory, useTheme, VariableOrigin } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { Button, DataLinkBuiltInVars, stylesFactory, useTheme } from '@grafana/ui';
import { GrafanaTheme, VariableOrigin } from '@grafana/data';
import { DerivedFieldConfig } from '../types';
import { DerivedField } from './DerivedField';
import { DebugSection } from './DebugSection';
......
......@@ -11,9 +11,15 @@ import { DataProcessor } from './data_processor';
import { axesEditorComponent } from './axes_editor';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import { VariableSuggestion } from '@grafana/ui';
import { getProcessedDataFrames } from 'app/features/dashboard/state/runRequest';
import { getColorFromHexRgbOrName, PanelEvents, DataFrame, DataLink, DateTimeInput } from '@grafana/data';
import {
getColorFromHexRgbOrName,
PanelEvents,
DataFrame,
DataLink,
DateTimeInput,
VariableSuggestion,
} from '@grafana/data';
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment