From 1284c596fe9437cd1d02f9ed4d538fbbd0d1cac3 Mon Sep 17 00:00:00 2001 From: Michael Mandrus <41969079+mmandrus@users.noreply.github.com> Date: Thu, 9 Jun 2022 11:54:57 -0400 Subject: [PATCH] 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 --- packages/grafana-data/src/geo/layer.ts | 8 ++- .../grafana-ui/src/utils/standardEditors.tsx | 2 +- .../app/plugins/panel/geomap/GeomapPanel.tsx | 31 +++++++--- .../panel/geomap/editor/layerEditor.tsx | 12 ++++ .../layers/data/FrameSelectionEditor.tsx | 62 +++++++++++++++++++ 5 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 public/app/plugins/panel/geomap/layers/data/FrameSelectionEditor.tsx diff --git a/packages/grafana-data/src/geo/layer.ts b/packages/grafana-data/src/geo/layer.ts index 60463aa7ebc..caa0360a672 100644 --- a/packages/grafana-data/src/geo/layer.ts +++ b/packages/grafana-data/src/geo/layer.ts @@ -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 { // 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 { */ export interface MapLayerHandler { init: () => BaseLayer; + /** + * The update function should only be implemented if the layer type makes use of query data + */ update?: (data: PanelData) => void; legend?: ReactNode; diff --git a/packages/grafana-ui/src/utils/standardEditors.tsx b/packages/grafana-ui/src/utils/standardEditors.tsx index 07948c2ae54..ca7007896ab 100644 --- a/packages/grafana-ui/src/utils/standardEditors.tsx +++ b/packages/grafana-ui/src/utils/standardEditors.tsx @@ -296,7 +296,7 @@ export const getStandardOptionEditors = () => { const fieldName: StandardEditorsRegistryItem = { id: 'field-name', name: 'Field name', - description: 'Time zone selection', + description: 'Allows selecting a field name from a data frame', editor: FieldNamePicker as any, }; diff --git a/public/app/plugins/panel/geomap/GeomapPanel.tsx b/public/app/plugins/panel/geomap/GeomapPanel.tsx index 76f3d6d9e0d..23d58e9702f 100644 --- a/public/app/plugins/panel/geomap/GeomapPanel.tsx +++ b/public/app/plugins/panel/geomap/GeomapPanel.tsx @@ -21,7 +21,9 @@ import { DataHoverClearEvent, DataHoverEvent, FrameGeometrySourceMode, + getFrameMatchers, GrafanaTheme, + MapLayerHandler, MapLayerOptions, PanelData, PanelProps, @@ -251,9 +253,7 @@ export class GeomapPanel extends Component { */ 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 { 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 { 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 { this.byName.set(UID, state); (state.layer as any).__state = state; + + this.applyLayerFilter(handler, options); + return state; } + applyLayerFilter(handler: MapLayerHandler, options: MapLayerOptions): 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): View { let view = new View({ center: [0, 0], diff --git a/public/app/plugins/panel/geomap/editor/layerEditor.tsx b/public/app/plugins/panel/geomap/editor/layerEditor.tsx index 3ddeb4f0bd3..755e3390ba3 100644 --- a/public/app/plugins/panel/geomap/editor/layerEditor.tsx +++ b/public/app/plugins/panel/geomap/editor/layerEditor.tsx @@ -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 { + 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> = ({ + 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) => { + 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 ( +