Geomap: Add ability to select a data query filter for each layer (#49966)

* Fix random typo I found

* add 'useDataFrame' boolean to each layer type to determine whether the layer queues off of query data

* Add data frame picker to options layout depending on layer type

* change layer update logic to render features from a specific data frame. Lift data frame selection up a level in order to add some more complex error handling.

* add a todo to the MapLayerRegistryItem interface

* update optional arg in function signature for consistency

* move dataframe filtering to paneldata, revert layers to prior state

* commit refactor, need to clean up still

* pull copy-pasted code into its own function

* clean up comments

* Update layer.ts

* remove unused types

* fix spacing

* improve dropdown

* need to add dependency to this callback function, otherwise it will always use the context of the last layer

* add data query recovery logic to handle query renames

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Michael Mandrus 2022-06-09 11:54:57 -04:00 committed by GitHub
parent 9703c9211e
commit 1284c596fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 103 additions and 12 deletions

View File

@ -3,7 +3,7 @@ import BaseLayer from 'ol/layer/Base';
import { ReactNode } from 'react';
import { GrafanaTheme2 } from '../themes';
import { PanelData } from '../types';
import { MatcherConfig, PanelData } from '../types';
import { PanelOptionsEditorBuilder } from '../utils';
import { RegistryItemWithOptions } from '../utils/Registry';
@ -58,6 +58,9 @@ export interface MapLayerOptions<TConfig = any> {
// Common method to define geometry fields
location?: FrameGeometrySource;
// Defines which data query refId is associated with the layer
filterData?: MatcherConfig;
// Common properties:
// https://openlayers.org/en/latest/apidoc/module-ol_layer_Base-BaseLayer.html
// Layer opacity (0-1)
@ -72,6 +75,9 @@ export interface MapLayerOptions<TConfig = any> {
*/
export interface MapLayerHandler<TConfig = any> {
init: () => BaseLayer;
/**
* The update function should only be implemented if the layer type makes use of query data
*/
update?: (data: PanelData) => void;
legend?: ReactNode;

View File

@ -296,7 +296,7 @@ export const getStandardOptionEditors = () => {
const fieldName: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
id: 'field-name',
name: 'Field name',
description: 'Time zone selection',
description: 'Allows selecting a field name from a data frame',
editor: FieldNamePicker as any,
};

View File

@ -21,7 +21,9 @@ import {
DataHoverClearEvent,
DataHoverEvent,
FrameGeometrySourceMode,
getFrameMatchers,
GrafanaTheme,
MapLayerHandler,
MapLayerOptions,
PanelData,
PanelProps,
@ -251,9 +253,7 @@ export class GeomapPanel extends Component<Props, State> {
*/
dataChanged(data: PanelData) {
for (const state of this.layers) {
if (state.handler.update) {
state.handler.update(data);
}
this.applyLayerFilter(state.handler, state.options);
}
}
@ -468,9 +468,7 @@ export class GeomapPanel extends Component<Props, State> {
group.setAt(layerIndex, info.layer);
// initialize with new data
if (info.handler.update) {
info.handler.update(this.props.data);
}
this.applyLayerFilter(info.handler, newOptions);
} catch (err) {
console.warn('ERROR', err);
return false;
@ -506,10 +504,6 @@ export class GeomapPanel extends Component<Props, State> {
const handler = await item.create(map, options, config.theme2);
const layer = handler.init();
if (handler.update) {
handler.update(this.props.data);
}
if (!options.name) {
options.name = this.getNextLayerName();
}
@ -533,9 +527,26 @@ export class GeomapPanel extends Component<Props, State> {
this.byName.set(UID, state);
(state.layer as any).__state = state;
this.applyLayerFilter(handler, options);
return state;
}
applyLayerFilter(handler: MapLayerHandler<any>, options: MapLayerOptions<any>): void {
if (handler.update) {
let panelData = this.props.data;
if (options.filterData) {
const matcherFunc = getFrameMatchers(options.filterData);
panelData = {
...panelData,
series: panelData.series.filter(matcherFunc),
};
}
handler.update(panelData);
}
}
initMapView(config: MapViewConfig, layers?: Collection<BaseLayer>): View {
let view = new View({
center: [0, 0],

View File

@ -6,6 +6,7 @@ import { hasAlphaPanels } from 'app/core/config';
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
import { addLocationFields } from 'app/features/geo/editor/locationEditor';
import { FrameSelectionEditor } from '../layers/data/FrameSelectionEditor';
import { defaultMarkersConfig } from '../layers/data/markersLayer';
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from '../layers/registry';
import { MapLayerState } from '../types';
@ -76,6 +77,17 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map
},
});
// Show data filter if the layer type can do something with the data query results
if (handler.update) {
builder.addCustomEditor({
id: 'filterData',
path: 'filterData',
name: 'Data',
editor: FrameSelectionEditor,
defaultValue: undefined,
});
}
if (!layer) {
return; // unknown layer type
}

View File

@ -0,0 +1,62 @@
import React, { FC, useCallback, useMemo, useState } from 'react';
import { FrameMatcherID, getFieldDisplayName, MatcherConfig, SelectableValue, StandardEditorProps } from '@grafana/data';
import { Select } from '@grafana/ui';
const recoverRefIdMissing = (newRefIds: SelectableValue[], oldRefIds: SelectableValue[], previousValue: string | undefined): SelectableValue | undefined => {
if (!previousValue) {
return;
}
// Previously selected value is missing from the new list.
// Find the value that is in the new list but isn't in the old list
let changedTo = newRefIds.find((refId) => {
return !oldRefIds.some((refId2) => {
return refId === refId2;
});
});
if (changedTo) {
// Found the new value, we assume the old value changed to this one, so we'll use it
return changedTo;
}
return;
};
export const FrameSelectionEditor: FC<StandardEditorProps<MatcherConfig>> = ({
value,
context,
onChange,
item,
}) => {
const listOfRefId = useMemo(() => {
return context.data.map(f => ({
value: f.refId,
label: `Query: ${f.refId} (size: ${f.length})`,
description: f.fields.map(f => getFieldDisplayName(f)).join(', '),
}));
}, [context.data]);
const [priorSelectionState, updatePriorSelectionState] = useState({
refIds: [] as SelectableValue[],
value: undefined as string | undefined,
});
const currentValue = useMemo(() => {
return listOfRefId.find((refId) => refId.value === value?.options) ?? recoverRefIdMissing(listOfRefId, priorSelectionState.refIds, priorSelectionState.value);
}, [value, listOfRefId])
const onFilterChange = useCallback((v: SelectableValue<string>) => {
onChange(v?.value ? {
"id": FrameMatcherID.byRefId,
"options": v.value
} : undefined);
}, [context.options.name]);
if (listOfRefId !== priorSelectionState.refIds || currentValue?.value !== priorSelectionState.value) {
updatePriorSelectionState({
refIds: listOfRefId,
value: currentValue?.value
});
}
return (
<Select options={listOfRefId} onChange={onFilterChange} isClearable={true} placeholder="Change filter" value={currentValue}/>
);
};