Skip to content

Commit

Permalink
Geomap: Improve location editor (#58017)
Browse files Browse the repository at this point in the history
* add custom component for location editor

* FC cleanup

* Apply filter to add location fields call

* Create custom editor for location mode

* Apply validation logic and render warning

* Improve alert styling

* Add help url button to location alert

* Add success alert for auto

* Remove completed TODOs

* Only use alert on error, not success

* Change location mode to dropdown

* Change alert severity to less severe, info

* Prevent auto field selection during manual

* Update location testing to be for auto mode

* Run geo transformer editor init once

* Fix breaking test

* Clean up some anys

* Update styling for alert

* Remove auto success styling

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
  • Loading branch information
3 people committed Nov 22, 2022
1 parent b875ca0 commit ee8f292
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 51 deletions.
6 changes: 0 additions & 6 deletions .betterer.results
Expand Up @@ -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"]
],
Expand Down
8 changes: 4 additions & 4 deletions 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';
Expand Down Expand Up @@ -28,15 +28,15 @@ export interface GazetteerPathEditorConfigSettings {
options?: Array<SelectableValue<string>>;
}

export const GazetteerPathEditor: FC<StandardEditorProps<string, any, any, GazetteerPathEditorConfigSettings>> = ({
export const GazetteerPathEditor = ({
value,
onChange,
context,
item,
}) => {
}: StandardEditorProps<string, GazetteerPathEditorConfigSettings>) => {
const styles = getStyles(useTheme2());
const [gaz, setGaz] = useState<Gazetteer>();
const settings = item.settings as any;
const settings = item.settings;

useEffect(() => {
async function fetchData() {
Expand Down
43 changes: 12 additions & 31 deletions public/app/features/geo/editor/locationEditor.ts
Expand Up @@ -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<TOptions>(
title: string,
prefix: string,
builder: PanelOptionsEditorBuilder<TOptions>,
source?: FrameGeometrySource
builder: PanelOptionsEditorBuilder<TOptions>, // ??? 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
Expand Down
126 changes: 126 additions & 0 deletions 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<string, ModeEditorSettings, unknown, unknown>) => {
const [info, setInfo] = useState<FrameGeometryField>();

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 (
<Alert
title={info.warning}
severity="warning"
buttonContent={<Icon name="question-circle" size="xl" />}
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 <span>{info.description}</span>;
}
}
return null;
};

return (
<>
<Select
options={MODE_OPTIONS}
value={value}
onChange={(v) => {
onChange(v.value);
}}
/>
<HorizontalGroup className={styles.hGroup}>{dataValidation()}</HorizontalGroup>
</>
);
};

const getStyles = (theme: GrafanaTheme2) => {
return {
alert: css`
& div {
padding: 4px;
}
margin-bottom: 0px;
margin-top: 5px;
padding: 2px;
`,
// TODO apply styling to horizontal group (currently not working)
hGroup: css`
& div {
width: 100%;
}
`,
};
};
8 changes: 6 additions & 2 deletions public/app/features/geo/utils/location.test.ts
Expand Up @@ -11,6 +11,10 @@ const geohash = ['9q94r', 'dr5rs'];
const names = ['A', 'B'];

describe('handle location parsing', () => {
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation();
});

it('auto should find geohash field', async () => {
const frame = toDataFrame({
name: 'simple',
Expand All @@ -20,7 +24,7 @@ describe('handle location parsing', () => {
],
});

const matchers = await getLocationMatchers();
const matchers = await getLocationMatchers({ mode: FrameGeometrySourceMode.Auto });
const fields = getLocationFields(frame, matchers);
expect(fields.mode).toEqual(FrameGeometrySourceMode.Geohash);
expect(fields.geohash).toBeDefined();
Expand Down Expand Up @@ -78,7 +82,7 @@ describe('handle location parsing', () => {
});

const matchers = await getLocationMatchers({
mode: FrameGeometrySourceMode.Geohash,
mode: FrameGeometrySourceMode.Auto,
});
const geo = getGeometryField(frame, matchers).field!;
expect(geo.values.toArray().map((p) => toLonLat((p as Point).getCoordinates()))).toMatchInlineSnapshot(`
Expand Down
22 changes: 17 additions & 5 deletions public/app/features/geo/utils/location.ts
Expand Up @@ -73,24 +73,32 @@ export async function getLocationMatchers(src?: FrameGeometrySource): Promise<Lo
...defaultMatchers,
mode: src?.mode ?? FrameGeometrySourceMode.Auto,
};
info.gazetteer = await getGazetteer(src?.gazetteer); // Always have gazetteer selected (or default) for smooth transition
switch (info.mode) {
case FrameGeometrySourceMode.Geohash:
if (src?.geohash) {
info.geohash = getFieldFinder(getFieldMatcher({ id: FieldMatcherID.byName, options: src.geohash }));
} else {
info.geohash = () => undefined; // In manual mode, don't automatically find field
}
break;
case FrameGeometrySourceMode.Lookup:
if (src?.lookup) {
info.lookup = getFieldFinder(getFieldMatcher({ id: FieldMatcherID.byName, options: src.lookup }));
} else {
info.lookup = () => undefined; // In manual mode, don't automatically find field
}
info.gazetteer = await getGazetteer(src?.gazetteer);
break;
case FrameGeometrySourceMode.Coords:
if (src?.latitude) {
info.latitude = getFieldFinder(getFieldMatcher({ id: FieldMatcherID.byName, options: src.latitude }));
} else {
info.latitude = () => undefined; // In manual mode, don't automatically find field
}
if (src?.longitude) {
info.longitude = getFieldFinder(getFieldMatcher({ id: FieldMatcherID.byName, options: src.longitude }));
} else {
info.longitude = () => undefined; // In manual mode, don't automatically find field
}
break;
}
Expand Down Expand Up @@ -132,7 +140,7 @@ export function getLocationFields(frame: DataFrame, location: LocationFieldMatch
fields.mode = FrameGeometrySourceMode.Geohash;
return fields;
}
fields.lookup = location.geohash(frame);
fields.lookup = location.lookup(frame);
if (fields.lookup) {
fields.mode = FrameGeometrySourceMode.Lookup;
return fields;
Expand All @@ -159,6 +167,7 @@ export interface FrameGeometryField {
field?: Field<Geometry | undefined>;
warning?: string;
derived?: boolean;
description?: string;
}

export function getGeometryField(frame: DataFrame, location: LocationFieldMatchers): FrameGeometryField {
Expand All @@ -179,21 +188,23 @@ export function getGeometryField(frame: DataFrame, location: LocationFieldMatche
return {
field: pointFieldFromLonLat(fields.longitude, fields.latitude),
derived: true,
description: `${fields.mode}: ${fields.latitude.name}, ${fields.longitude.name}`,
};
}
return {
warning: 'Missing latitude/longitude fields',
warning: 'Select latitude/longitude fields',
};

case FrameGeometrySourceMode.Geohash:
if (fields.geohash) {
return {
field: pointFieldFromGeohash(fields.geohash),
derived: true,
description: `${fields.mode}`,
};
}
return {
warning: 'Missing geohash field',
warning: 'Select geohash field',
};

case FrameGeometrySourceMode.Lookup:
Expand All @@ -202,14 +213,15 @@ export function getGeometryField(frame: DataFrame, location: LocationFieldMatche
return {
field: getGeoFieldFromGazetteer(location.gazetteer, fields.lookup),
derived: true,
description: `${fields.mode}: ${location.gazetteer.path}`, // TODO get better name for this
};
}
return {
warning: 'Gazetteer not found',
};
}
return {
warning: 'Missing lookup field',
warning: 'Select lookup field',
};
}

Expand Down
Expand Up @@ -122,7 +122,8 @@ export const SetGeometryTransformerEditor: React.FC<TransformerUIProps<SpatialTr
props.onChange({ ...opts, ...props.options });
console.log('geometry useEffect', opts);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const styles = getStyles(useTheme2());

Expand Down
11 changes: 9 additions & 2 deletions public/app/plugins/panel/geomap/editor/layerEditor.tsx
@@ -1,6 +1,6 @@
import { get as lodashGet, isEqual } from 'lodash';

import { FrameGeometrySourceMode, MapLayerOptions } from '@grafana/data';
import { FrameGeometrySourceMode, getFrameMatchers, MapLayerOptions } from '@grafana/data';
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
import { addLocationFields } from 'app/features/geo/editor/locationEditor';
Expand Down Expand Up @@ -96,7 +96,14 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map
}

if (layer.showLocation) {
addLocationFields('Location', 'location.', builder, options.location);
let data = context.data;
// If `filterData` exists filter data feeding into location editor
if (options.filterData) {
const matcherFunc = getFrameMatchers(options.filterData);
data = data.filter(matcherFunc);
}

addLocationFields('Location', 'location.', builder, options.location, data);
}
if (handler.registerOptionsUI) {
handler.registerOptionsUI(builder);
Expand Down

0 comments on commit ee8f292

Please sign in to comment.