diff --git a/.betterer.results b/.betterer.results index b05e872e997b..21865cd8692f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3995,12 +3995,6 @@ exports[`better eslint`] = { "public/app/features/folders/state/actions.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/geo/editor/GazetteerPathEditor.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], "public/app/features/geo/format/geohash.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/features/geo/editor/GazetteerPathEditor.tsx b/public/app/features/geo/editor/GazetteerPathEditor.tsx index c85fe657f5fd..067b488956cc 100644 --- a/public/app/features/geo/editor/GazetteerPathEditor.tsx +++ b/public/app/features/geo/editor/GazetteerPathEditor.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React, { FC, useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import { StandardEditorProps, SelectableValue, GrafanaTheme2 } from '@grafana/data'; import { Alert, Select, stylesFactory, useTheme2 } from '@grafana/ui'; @@ -28,15 +28,15 @@ export interface GazetteerPathEditorConfigSettings { options?: Array>; } -export const GazetteerPathEditor: FC> = ({ +export const GazetteerPathEditor = ({ value, onChange, context, item, -}) => { +}: StandardEditorProps) => { const styles = getStyles(useTheme2()); const [gaz, setGaz] = useState(); - const settings = item.settings as any; + const settings = item.settings; useEffect(() => { async function fetchData() { diff --git a/public/app/features/geo/editor/locationEditor.ts b/public/app/features/geo/editor/locationEditor.ts index 0358b3eb8eff..5f7d6da044f9 100644 --- a/public/app/features/geo/editor/locationEditor.ts +++ b/public/app/features/geo/editor/locationEditor.ts @@ -4,47 +4,28 @@ import { FrameGeometrySource, FrameGeometrySourceMode, PanelOptionsEditorBuilder, + DataFrame, } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors/src'; import { GazetteerPathEditor } from 'app/features/geo/editor/GazetteerPathEditor'; +import { LocationModeEditor } from './locationModeEditor'; + export function addLocationFields( title: string, prefix: string, - builder: PanelOptionsEditorBuilder, - source?: FrameGeometrySource + builder: PanelOptionsEditorBuilder, // ??? Perhaps pass in the filtered data? + source?: FrameGeometrySource, + data?: DataFrame[] ) { - builder.addRadio({ + builder.addCustomEditor({ + id: 'modeEditor', path: `${prefix}mode`, - name: title, - description: '', - defaultValue: FrameGeometrySourceMode.Auto, - settings: { - options: [ - { - value: FrameGeometrySourceMode.Auto, - label: 'Auto', - ariaLabel: selectors.components.Transforms.SpatialOperations.location.autoOption, - }, - { - value: FrameGeometrySourceMode.Coords, - label: 'Coords', - ariaLabel: selectors.components.Transforms.SpatialOperations.location.coords.option, - }, - { - value: FrameGeometrySourceMode.Geohash, - label: 'Geohash', - ariaLabel: selectors.components.Transforms.SpatialOperations.location.geohash.option, - }, - { - value: FrameGeometrySourceMode.Lookup, - label: 'Lookup', - ariaLabel: selectors.components.Transforms.SpatialOperations.location.lookup.option, - }, - ], - }, + name: 'Location Mode', + editor: LocationModeEditor, + settings: { data, source }, }); + // TODO apply data filter to field pickers switch (source?.mode) { case FrameGeometrySourceMode.Coords: builder diff --git a/public/app/features/geo/editor/locationModeEditor.tsx b/public/app/features/geo/editor/locationModeEditor.tsx new file mode 100644 index 000000000000..34cc551f4cdb --- /dev/null +++ b/public/app/features/geo/editor/locationModeEditor.tsx @@ -0,0 +1,126 @@ +import { css } from '@emotion/css'; +import React, { useEffect, useState } from 'react'; + +import { + StandardEditorProps, + FrameGeometrySourceMode, + DataFrame, + FrameGeometrySource, + GrafanaTheme2, +} from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { Alert, HorizontalGroup, Icon, Select, useStyles2 } from '@grafana/ui'; + +import { FrameGeometryField, getGeometryField, getLocationMatchers } from '../utils/location'; + +const MODE_OPTIONS = [ + { + value: FrameGeometrySourceMode.Auto, + label: 'Auto', + ariaLabel: selectors.components.Transforms.SpatialOperations.location.autoOption, + description: 'Automatically identify location data based on default field names', + }, + { + value: FrameGeometrySourceMode.Coords, + label: 'Coords', + ariaLabel: selectors.components.Transforms.SpatialOperations.location.coords.option, + description: 'Specify latitude and longitude fields', + }, + { + value: FrameGeometrySourceMode.Geohash, + label: 'Geohash', + ariaLabel: selectors.components.Transforms.SpatialOperations.location.geohash.option, + description: 'Specify geohash field', + }, + { + value: FrameGeometrySourceMode.Lookup, + label: 'Lookup', + ariaLabel: selectors.components.Transforms.SpatialOperations.location.lookup.option, + description: 'Specify Gazetteer and lookup field', + }, +]; + +interface ModeEditorSettings { + data?: DataFrame[]; + source?: FrameGeometrySource; +} + +const helpUrl = 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/geomap/#location'; + +export const LocationModeEditor = ({ + value, + onChange, + context, + item, +}: StandardEditorProps) => { + const [info, setInfo] = useState(); + + useEffect(() => { + if (item.settings?.source && item.settings?.data?.length && item.settings.data[0]) { + getLocationMatchers(item.settings.source).then((location) => { + if (item.settings && item.settings.data) { + setInfo(getGeometryField(item.settings.data[0], location)); + } + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [item.settings]); + + const styles = useStyles2(getStyles); + + const dataValidation = () => { + if (info) { + if (info.warning) { + return ( + } + className={styles.alert} + onRemove={() => { + const newWindow = window.open(helpUrl, '_blank', 'noopener,noreferrer'); + if (newWindow) { + newWindow.opener = null; + } + }} + /> + ); + } else if (value === FrameGeometrySourceMode.Auto && info.description) { + return {info.description}; + } + } + return null; + }; + + return ( + <> +