mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -3,7 +3,7 @@ import BaseLayer from 'ol/layer/Base';
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '../themes';
|
import { GrafanaTheme2 } from '../themes';
|
||||||
import { PanelData } from '../types';
|
import { MatcherConfig, PanelData } from '../types';
|
||||||
import { PanelOptionsEditorBuilder } from '../utils';
|
import { PanelOptionsEditorBuilder } from '../utils';
|
||||||
import { RegistryItemWithOptions } from '../utils/Registry';
|
import { RegistryItemWithOptions } from '../utils/Registry';
|
||||||
|
|
||||||
@@ -58,6 +58,9 @@ export interface MapLayerOptions<TConfig = any> {
|
|||||||
// Common method to define geometry fields
|
// Common method to define geometry fields
|
||||||
location?: FrameGeometrySource;
|
location?: FrameGeometrySource;
|
||||||
|
|
||||||
|
// Defines which data query refId is associated with the layer
|
||||||
|
filterData?: MatcherConfig;
|
||||||
|
|
||||||
// Common properties:
|
// Common properties:
|
||||||
// https://openlayers.org/en/latest/apidoc/module-ol_layer_Base-BaseLayer.html
|
// https://openlayers.org/en/latest/apidoc/module-ol_layer_Base-BaseLayer.html
|
||||||
// Layer opacity (0-1)
|
// Layer opacity (0-1)
|
||||||
@@ -72,6 +75,9 @@ export interface MapLayerOptions<TConfig = any> {
|
|||||||
*/
|
*/
|
||||||
export interface MapLayerHandler<TConfig = any> {
|
export interface MapLayerHandler<TConfig = any> {
|
||||||
init: () => BaseLayer;
|
init: () => BaseLayer;
|
||||||
|
/**
|
||||||
|
* The update function should only be implemented if the layer type makes use of query data
|
||||||
|
*/
|
||||||
update?: (data: PanelData) => void;
|
update?: (data: PanelData) => void;
|
||||||
legend?: ReactNode;
|
legend?: ReactNode;
|
||||||
|
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ export const getStandardOptionEditors = () => {
|
|||||||
const fieldName: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
|
const fieldName: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
|
||||||
id: 'field-name',
|
id: 'field-name',
|
||||||
name: 'Field name',
|
name: 'Field name',
|
||||||
description: 'Time zone selection',
|
description: 'Allows selecting a field name from a data frame',
|
||||||
editor: FieldNamePicker as any,
|
editor: FieldNamePicker as any,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import {
|
|||||||
DataHoverClearEvent,
|
DataHoverClearEvent,
|
||||||
DataHoverEvent,
|
DataHoverEvent,
|
||||||
FrameGeometrySourceMode,
|
FrameGeometrySourceMode,
|
||||||
|
getFrameMatchers,
|
||||||
GrafanaTheme,
|
GrafanaTheme,
|
||||||
|
MapLayerHandler,
|
||||||
MapLayerOptions,
|
MapLayerOptions,
|
||||||
PanelData,
|
PanelData,
|
||||||
PanelProps,
|
PanelProps,
|
||||||
@@ -251,9 +253,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
*/
|
*/
|
||||||
dataChanged(data: PanelData) {
|
dataChanged(data: PanelData) {
|
||||||
for (const state of this.layers) {
|
for (const state of this.layers) {
|
||||||
if (state.handler.update) {
|
this.applyLayerFilter(state.handler, state.options);
|
||||||
state.handler.update(data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,9 +468,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
group.setAt(layerIndex, info.layer);
|
group.setAt(layerIndex, info.layer);
|
||||||
|
|
||||||
// initialize with new data
|
// initialize with new data
|
||||||
if (info.handler.update) {
|
this.applyLayerFilter(info.handler, newOptions);
|
||||||
info.handler.update(this.props.data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('ERROR', err);
|
console.warn('ERROR', err);
|
||||||
return false;
|
return false;
|
||||||
@@ -506,10 +504,6 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
const handler = await item.create(map, options, config.theme2);
|
const handler = await item.create(map, options, config.theme2);
|
||||||
const layer = handler.init();
|
const layer = handler.init();
|
||||||
|
|
||||||
if (handler.update) {
|
|
||||||
handler.update(this.props.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.name) {
|
if (!options.name) {
|
||||||
options.name = this.getNextLayerName();
|
options.name = this.getNextLayerName();
|
||||||
}
|
}
|
||||||
@@ -533,9 +527,26 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
|
|
||||||
this.byName.set(UID, state);
|
this.byName.set(UID, state);
|
||||||
(state.layer as any).__state = state;
|
(state.layer as any).__state = state;
|
||||||
|
|
||||||
|
this.applyLayerFilter(handler, options);
|
||||||
|
|
||||||
return state;
|
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 {
|
initMapView(config: MapViewConfig, layers?: Collection<BaseLayer>): View {
|
||||||
let view = new View({
|
let view = new View({
|
||||||
center: [0, 0],
|
center: [0, 0],
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { hasAlphaPanels } from 'app/core/config';
|
|||||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||||
import { addLocationFields } from 'app/features/geo/editor/locationEditor';
|
import { addLocationFields } from 'app/features/geo/editor/locationEditor';
|
||||||
|
|
||||||
|
import { FrameSelectionEditor } from '../layers/data/FrameSelectionEditor';
|
||||||
import { defaultMarkersConfig } from '../layers/data/markersLayer';
|
import { defaultMarkersConfig } from '../layers/data/markersLayer';
|
||||||
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from '../layers/registry';
|
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from '../layers/registry';
|
||||||
import { MapLayerState } from '../types';
|
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) {
|
if (!layer) {
|
||||||
return; // unknown layer type
|
return; // unknown layer type
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}/>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user