GraphNG: adding possibility to toggle tooltip, graph and legend for series (#29575)

This commit is contained in:
Marcus Andersson
2021-01-06 21:40:32 +01:00
committed by GitHub
parent 78d72007d8
commit 7aa6926ef6
32 changed files with 1264 additions and 51 deletions

View File

@@ -102,6 +102,7 @@ export class GrafanaApp {
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
standardTransformersRegistry.setInit(getStandardTransformers);
variableAdapters.setInit(getDefaultVariableAdapters);
setVariableQueryRunner(new VariableQueryRunner());
app.config(

View File

@@ -11,6 +11,7 @@ interface DynamicConfigValueEditorProps {
context: FieldOverrideContext;
onRemove: () => void;
isCollapsible?: boolean;
isSystemOverride?: boolean;
}
export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> = ({
@@ -20,6 +21,7 @@ export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> =
onChange,
onRemove,
isCollapsible,
isSystemOverride,
}) => {
const theme = useTheme();
const styles = getStyles(theme);
@@ -37,9 +39,11 @@ export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> =
{item.name}
{!isExpanded && includeCounter && item.getItemsCount && <Counter value={item.getItemsCount(property.value)} />}
</Label>
<div>
<IconButton name="times" onClick={onRemove} />
</div>
{!isSystemOverride && (
<div>
<IconButton name="times" onClick={onRemove} />
</div>
)}
</HorizontalGroup>
);

View File

@@ -6,6 +6,7 @@ import {
FieldConfigOptionsRegistry,
FieldConfigProperty,
GrafanaTheme,
isSystemOverride as isSystemOverrideGuard,
VariableSuggestionsScope,
} from '@grafana/data';
import {
@@ -136,6 +137,8 @@ export const OverrideEditor: React.FC<OverrideEditorProps> = ({
);
};
const isSystemOverride = isSystemOverrideGuard(override);
return (
<OptionsGroup renderTitle={renderOverrideTitle} id={name} key={name}>
<Field label={matcherLabel}>
@@ -150,6 +153,7 @@ export const OverrideEditor: React.FC<OverrideEditorProps> = ({
<>
{override.properties.map((p, j) => {
const item = registry.getIfExists(p.id);
console.log('item', item);
if (!item) {
return <div>Unknown property: {p.id}</div>;
@@ -162,6 +166,7 @@ export const OverrideEditor: React.FC<OverrideEditorProps> = ({
<DynamicConfigValueEditor
key={`${p.id}/${j}`}
isCollapsible={isCollapsible}
isSystemOverride={isSystemOverride}
onChange={value => onDynamicConfigValueChange(j, value)}
onRemove={() => onDynamicConfigValueRemove(j)}
property={p}
@@ -173,7 +178,7 @@ export const OverrideEditor: React.FC<OverrideEditorProps> = ({
/>
);
})}
{override.matcher.options && (
{!isSystemOverride && override.matcher.options && (
<div className={styles.propertyPickerWrapper}>
<ValuePicker
label="Add override property"

View File

@@ -82,6 +82,7 @@ export const OverrideFieldConfigEditor: React.FC<Props> = props => {
variant="secondary"
options={fieldMatchersUI
.list()
.filter(o => !o.excludeFromPicker)
.map<SelectableValue<string>>(i => ({ label: i.name, value: i.id, description: i.description }))}
onChange={value => onOverrideAdd(value)}
isFullWidth={false}

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { TooltipPlugin, ZoomPlugin, GraphNG } from '@grafana/ui';
import React, { useCallback } from 'react';
import { TooltipPlugin, ZoomPlugin, GraphNG, GraphNGLegendEvent } from '@grafana/ui';
import { PanelProps } from '@grafana/data';
import { Options } from './types';
import { AnnotationsPlugin } from './plugins/AnnotationsPlugin';
import { ExemplarsPlugin } from './plugins/ExemplarsPlugin';
import { hideSeriesConfigFactory } from './hideSeriesConfigFactory';
import { ContextMenuPlugin } from './plugins/ContextMenuPlugin';
interface GraphPanelProps extends PanelProps<Options> {}
@@ -15,9 +16,18 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
width,
height,
options,
fieldConfig,
onChangeTimeRange,
onFieldConfigChange,
replaceVariables,
}) => {
const onLegendClick = useCallback(
(event: GraphNGLegendEvent) => {
onFieldConfigChange(hideSeriesConfigFactory(event, fieldConfig, data.series));
},
[fieldConfig, onFieldConfigChange, data.series]
);
return (
<GraphNG
data={data.series}
@@ -26,6 +36,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
width={width}
height={height}
legend={options.legend}
onLegendClick={onLegendClick}
>
<TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} />
<ZoomPlugin onZoom={onChangeTimeRange} />

View File

@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import _ from 'lodash';
import { FilterPill, HorizontalGroup } from '@grafana/ui';
import { FieldConfigEditorProps } from '@grafana/data';
import { HideSeriesConfig } from '@grafana/ui/src/components/uPlot/config';
export const SeriesConfigEditor: React.FC<FieldConfigEditorProps<HideSeriesConfig, {}>> = props => {
const { value, onChange } = props;
const onChangeToggle = useCallback(
(prop: keyof HideSeriesConfig) => {
onChange({ ...value, [prop]: !value[prop] });
},
[value, onChange]
);
return (
<HorizontalGroup spacing="xs">
{Object.keys(value).map((key: keyof HideSeriesConfig) => {
return (
<FilterPill
icon={value[key] ? 'eye-slash' : 'eye'}
onClick={() => onChangeToggle(key)}
key={key}
label={_.startCase(key)}
selected={value[key]}
/>
);
})}
</HorizontalGroup>
);
};

View File

@@ -0,0 +1,419 @@
import {
ByNamesMatcherMode,
DataFrame,
FieldConfigSource,
FieldMatcherID,
FieldType,
toDataFrame,
} from '@grafana/data';
import { GraphNGLegendEvent, GraphNGLegendEventMode } from '@grafana/ui';
import { hideSeriesConfigFactory } from './hideSeriesConfigFactory';
describe('hideSeriesConfigFactory', () => {
it('should create config override matching one series', () => {
const event: GraphNGLegendEvent = {
mode: GraphNGLegendEventMode.ToggleSelection,
fieldIndex: {
frameIndex: 0,
fieldIndex: 1,
},
};
const existingConfig: FieldConfigSource = {
defaults: {},
overrides: [],
};
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
];
const config = hideSeriesConfigFactory(event, existingConfig, data);
expect(config).toEqual({
defaults: {},
overrides: [createOverride(['temperature'])],
});
});
it('should create config override matching one series if selected with others', () => {
const event: GraphNGLegendEvent = {
mode: GraphNGLegendEventMode.ToggleSelection,
fieldIndex: {
frameIndex: 0,
fieldIndex: 1,
},
};
const existingConfig: FieldConfigSource = {
defaults: {},
overrides: [createOverride(['temperature', 'humidity'])],
};
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'pressure', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
];
const config = hideSeriesConfigFactory(event, existingConfig, data);
expect(config).toEqual({
defaults: {},
overrides: [createOverride(['temperature'])],
});
});
it('should create config override that append series to existing override', () => {
const event: GraphNGLegendEvent = {
mode: GraphNGLegendEventMode.AppendToSelection,
fieldIndex: {
frameIndex: 1,
fieldIndex: 1,
},
};
const existingConfig: FieldConfigSource = {
defaults: {},
overrides: [createOverride(['temperature'])],
};
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'pressure', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
];
const config = hideSeriesConfigFactory(event, existingConfig, data);
expect(config).toEqual({
defaults: {},
overrides: [createOverride(['temperature', 'humidity'])],
});
});
it('should create config override that hides all series if appending only existing series', () => {
const event: GraphNGLegendEvent = {
mode: GraphNGLegendEventMode.AppendToSelection,
fieldIndex: {
frameIndex: 0,
fieldIndex: 1,
},
};
const existingConfig: FieldConfigSource = {
defaults: {},
overrides: [createOverride(['temperature'])],
};
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
];
const config = hideSeriesConfigFactory(event, existingConfig, data);
expect(config).toEqual({
defaults: {},
overrides: [createOverride([])],
});
});
it('should create config override that removes series if appending existing field', () => {
const event: GraphNGLegendEvent = {
mode: GraphNGLegendEventMode.AppendToSelection,
fieldIndex: {
frameIndex: 0,
fieldIndex: 1,
},
};
const existingConfig: FieldConfigSource = {
defaults: {},
overrides: [createOverride(['temperature', 'humidity'])],
};
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
];
const config = hideSeriesConfigFactory(event, existingConfig, data);
expect(config).toEqual({
defaults: {},
overrides: [createOverride(['humidity'])],
});
});
it('should create config override replacing existing series', () => {
const event: GraphNGLegendEvent = {
mode: GraphNGLegendEventMode.ToggleSelection,
fieldIndex: {
frameIndex: 1,
fieldIndex: 1,
},
};
const existingConfig: FieldConfigSource = {
defaults: {},
overrides: [createOverride(['temperature'])],
};
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
];
const config = hideSeriesConfigFactory(event, existingConfig, data);
expect(config).toEqual({
defaults: {},
overrides: [createOverride(['humidity'])],
});
});
it('should create config override removing existing series', () => {
const event: GraphNGLegendEvent = {
mode: GraphNGLegendEventMode.ToggleSelection,
fieldIndex: {
frameIndex: 0,
fieldIndex: 1,
},
};
const existingConfig: FieldConfigSource = {
defaults: {},
overrides: [createOverride(['temperature'])],
};
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
];
const config = hideSeriesConfigFactory(event, existingConfig, data);
expect(config).toEqual({
defaults: {},
overrides: [],
});
});
it('should remove override if all fields are appended', () => {
const event: GraphNGLegendEvent = {
mode: GraphNGLegendEventMode.AppendToSelection,
fieldIndex: {
frameIndex: 1,
fieldIndex: 1,
},
};
const existingConfig: FieldConfigSource = {
defaults: {},
overrides: [createOverride(['temperature'])],
};
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
];
const config = hideSeriesConfigFactory(event, existingConfig, data);
expect(config).toEqual({
defaults: {},
overrides: [],
});
});
it('should create config override hiding appended series if no previous override exists', () => {
const event: GraphNGLegendEvent = {
mode: GraphNGLegendEventMode.AppendToSelection,
fieldIndex: {
frameIndex: 0,
fieldIndex: 1,
},
};
const existingConfig: FieldConfigSource = {
defaults: {},
overrides: [],
};
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'pressure', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
];
const config = hideSeriesConfigFactory(event, existingConfig, data);
expect(config).toEqual({
defaults: {},
overrides: [createOverride(['humidity', 'pressure'])],
});
});
it('should return existing override if invalid index is passed', () => {
const event: GraphNGLegendEvent = {
mode: GraphNGLegendEventMode.ToggleSelection,
fieldIndex: {
frameIndex: 4,
fieldIndex: 1,
},
};
const existingConfig: FieldConfigSource = {
defaults: {},
overrides: [createOverride(['temperature'])],
};
const data: DataFrame[] = [
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] },
{ name: 'humidity', type: FieldType.number, values: [1, 3, 5, 7] },
],
}),
];
const config = hideSeriesConfigFactory(event, existingConfig, data);
expect(config).toEqual({
defaults: {},
overrides: [createOverride(['temperature'])],
});
});
});
const createOverride = (matchers: string[]) => {
return {
__systemRef: 'hideSeriesFrom',
matcher: {
id: FieldMatcherID.byNames,
options: {
mode: ByNamesMatcherMode.exclude,
names: matchers,
prefix: 'All except:',
readOnly: true,
},
},
properties: [
{
id: 'custom.hideFrom',
value: {
graph: true,
legend: false,
tooltip: false,
},
},
],
};
};

View File

@@ -0,0 +1,175 @@
import {
ByNamesMatcherMode,
DataFrame,
DynamicConfigValue,
FieldConfigSource,
FieldMatcherID,
FieldType,
getFieldDisplayName,
isSystemOverrideWithRef,
SystemConfigOverrideRule,
} from '@grafana/data';
import { GraphNGLegendEvent, GraphNGLegendEventMode } from '@grafana/ui';
const displayOverrideRef = 'hideSeriesFrom';
const isHideSeriesOverride = isSystemOverrideWithRef(displayOverrideRef);
export const hideSeriesConfigFactory = (
event: GraphNGLegendEvent,
fieldConfig: FieldConfigSource<any>,
data: DataFrame[]
): FieldConfigSource<any> => {
const { fieldIndex, mode } = event;
const { overrides } = fieldConfig;
const frame = data[fieldIndex.frameIndex];
if (!frame) {
return fieldConfig;
}
const field = frame.fields[fieldIndex.fieldIndex];
if (!field) {
return fieldConfig;
}
const displayName = getFieldDisplayName(field, frame, data);
const currentIndex = overrides.findIndex(isHideSeriesOverride);
if (currentIndex < 0) {
if (mode === GraphNGLegendEventMode.ToggleSelection) {
const override = createOverride([displayName]);
return {
...fieldConfig,
overrides: [override, ...fieldConfig.overrides],
};
}
const displayNames = getDisplayNames(data, displayName);
const override = createOverride(displayNames);
return {
...fieldConfig,
overrides: [override, ...fieldConfig.overrides],
};
}
const overridesCopy = Array.from(overrides);
const [current] = overridesCopy.splice(currentIndex, 1) as SystemConfigOverrideRule[];
if (mode === GraphNGLegendEventMode.ToggleSelection) {
const existing = getExistingDisplayNames(current);
if (existing[0] === displayName && existing.length === 1) {
return {
...fieldConfig,
overrides: overridesCopy,
};
}
const override = createOverride([displayName]);
return {
...fieldConfig,
overrides: [override, ...overridesCopy],
};
}
const override = createExtendedOverride(current, displayName);
if (allFieldsAreExcluded(override, data)) {
return {
...fieldConfig,
overrides: overridesCopy,
};
}
return {
...fieldConfig,
overrides: [override, ...overridesCopy],
};
};
const createExtendedOverride = (current: SystemConfigOverrideRule, displayName: string): SystemConfigOverrideRule => {
const property = current.properties.find(p => p.id === 'custom.hideFrom');
const existing = getExistingDisplayNames(current);
const index = existing.findIndex(name => name === displayName);
if (index < 0) {
existing.push(displayName);
} else {
existing.splice(index, 1);
}
return createOverride(existing, property);
};
const getExistingDisplayNames = (rule: SystemConfigOverrideRule): string[] => {
const names = rule.matcher.options?.names;
if (!Array.isArray(names)) {
return [];
}
return names;
};
const createOverride = (names: string[], property?: DynamicConfigValue): SystemConfigOverrideRule => {
property = property ?? {
id: 'custom.hideFrom',
value: {
graph: true,
legend: false,
tooltip: false,
},
};
return {
__systemRef: displayOverrideRef,
matcher: {
id: FieldMatcherID.byNames,
options: {
mode: ByNamesMatcherMode.exclude,
names: names,
prefix: 'All except:',
readOnly: true,
},
},
properties: [
{
...property,
value: {
graph: true,
legend: false,
tooltip: false,
},
},
],
};
};
const allFieldsAreExcluded = (override: SystemConfigOverrideRule, data: DataFrame[]): boolean => {
return getExistingDisplayNames(override).length === getDisplayNames(data).length;
};
const getDisplayNames = (data: DataFrame[], excludeName?: string): string[] => {
const unique = new Set<string>();
for (const frame of data) {
for (const field of frame.fields) {
if (field.type !== FieldType.number) {
continue;
}
const name = getFieldDisplayName(field, frame, data);
if (name === excludeName) {
continue;
}
unique.add(name);
}
}
return Array.from(unique);
};

View File

@@ -15,6 +15,7 @@ import {
ScaleDistribution,
ScaleDistributionConfig,
} from '@grafana/ui';
import { SeriesConfigEditor } from './HideSeriesConfigEditor';
import { GraphPanel } from './GraphPanel';
import { graphPanelChangedHandler } from './migrations';
import { Options } from './types';
@@ -154,6 +155,23 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel)
defaultValue: { type: ScaleDistribution.Linear },
shouldApply: f => f.type === FieldType.number,
process: identityOverrideProcessor,
})
.addCustomEditor({
id: 'hideFrom',
name: 'Hide in area',
category: ['Series'],
path: 'hideFrom',
defaultValue: {
tooltip: false,
graph: false,
legend: false,
},
editor: SeriesConfigEditor,
override: SeriesConfigEditor,
shouldApply: () => true,
hideFromDefaults: true,
hideFromOverrides: true,
process: value => value,
});
},
})