Core: move dimensions out of geomap into app/features (#37946)

This commit is contained in:
Ryan McKinley 2021-08-17 13:43:54 -07:00 committed by GitHub
parent d93d989a5a
commit 2ff4b028c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 502 additions and 56 deletions

View File

@ -1,9 +1,9 @@
import React, { useCallback } from 'react';
import { FieldConfigEditorProps, StringFieldConfigSettings } from '@grafana/data';
import { StandardEditorProps, StringFieldConfigSettings } from '@grafana/data';
import { Input } from '../Input/Input';
import { TextArea } from '../TextArea/TextArea';
export const StringValueEditor: React.FC<FieldConfigEditorProps<string, StringFieldConfigSettings>> = ({
export const StringValueEditor: React.FC<StandardEditorProps<string, StringFieldConfigSettings>> = ({
value,
onChange,
item,

View File

@ -1,13 +1,13 @@
import { DataFrame, getFieldColorModeForField, getScaleCalculator, GrafanaTheme2 } from '@grafana/data';
import { ColorDimensionConfig, DimensionSupplier } from './types';
import { findField } from './utils';
import { findField, getLastNotNullFieldValue } from './utils';
//---------------------------------------------------------
// Color dimension
//---------------------------------------------------------
export function getColorDimension(
frame: DataFrame,
frame: DataFrame | undefined,
config: ColorDimensionConfig,
theme: GrafanaTheme2
): DimensionSupplier<string> {
@ -17,6 +17,7 @@ export function getColorDimension(
return {
isAssumed: Boolean(config.field?.length) || !config.fixed,
fixed: v,
value: () => v,
get: (i) => v,
};
}
@ -25,6 +26,7 @@ export function getColorDimension(
const fixed = mode.getCalculator(field, theme)(0, 0);
return {
fixed,
value: () => fixed,
get: (i) => fixed,
field,
};
@ -36,5 +38,6 @@ export function getColorDimension(
return scale(val).color;
},
field,
value: () => scale(getLastNotNullFieldValue(field)).color,
};
}

View File

@ -5,7 +5,7 @@ import { Select, ColorPicker, useStyles2 } from '@grafana/ui';
import {
useFieldDisplayNames,
useSelectOptions,
} from '../../../../../../../packages/grafana-ui/src/components/MatchersUI/utils';
} from '../../../../../packages/grafana-ui/src/components/MatchersUI/utils';
import { css } from '@emotion/css';
const fixedColorOption: SelectableValue<string> = {

View File

@ -0,0 +1,43 @@
import React, { useState, useEffect } from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
interface Props {
value: string;
onChange: (v: string) => void;
}
const IconSelector: React.FC<Props> = ({ value, onChange }) => {
const [icons, setIcons] = useState<SelectableValue[]>(value ? [{ value, label: value }] : []);
const [icon, setIcon] = useState<string>();
const iconRoot = (window as any).__grafana_public_path__ + 'img/icons/unicons/';
const onChangeIcon = (value: string) => {
onChange(value);
setIcon(value);
};
useEffect(() => {
getBackendSrv()
.get(`${iconRoot}/index.json`)
.then((data) => {
setIcons(
data.files.map((icon: string) => ({
value: icon,
label: icon,
}))
);
});
}, [iconRoot]);
return (
<Select
menuShouldPortal
options={icons}
value={icon}
onChange={(selectedValue) => {
onChangeIcon(selectedValue.value!);
}}
/>
);
};
export default IconSelector;

View File

@ -0,0 +1,115 @@
import React, { FC, useCallback } from 'react';
import {
FieldNamePickerConfigSettings,
StandardEditorProps,
StandardEditorsRegistryItem,
StringFieldConfigSettings,
} from '@grafana/data';
import { ResourceDimensionConfig, ResourceDimensionMode, ResourceDimensionOptions } from '../types';
import { InlineField, InlineFieldRow, RadioButtonGroup, StringValueEditor } from '@grafana/ui';
import { FieldNamePicker } from '../../../../../packages/grafana-ui/src/components/MatchersUI/FieldNamePicker';
import IconSelector from './IconSelector';
const resourceOptions = [
{ label: 'Fixed', value: ResourceDimensionMode.Fixed, description: 'Fixed value' },
{ label: 'Field', value: ResourceDimensionMode.Field, description: 'Use a string field result' },
// { label: 'Mapping', value: ResourceDimensionMode.Mapping, description: 'Map the results of a value to an svg' },
];
const dummyFieldSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
settings: {},
} as any;
const dummyImageStringSettings: StandardEditorsRegistryItem<string, StringFieldConfigSettings> = {
settings: {
placeholder: 'Enter image URL',
},
} as any;
export const ResourceDimensionEditor: FC<
StandardEditorProps<ResourceDimensionConfig, ResourceDimensionOptions, any>
> = (props) => {
const { value, context, onChange, item } = props;
const resourceType = item.settings?.resourceType ?? 'icon';
const labelWidth = 9;
const onModeChange = useCallback(
(mode) => {
onChange({
...value,
mode,
});
},
[onChange, value]
);
const onFieldChange = useCallback(
(field) => {
onChange({
...value,
field,
});
},
[onChange, value]
);
const onFixedChange = useCallback(
(fixed) => {
onChange({
...value,
fixed,
});
},
[onChange, value]
);
const mode = value?.mode ?? ResourceDimensionMode.Fixed;
return (
<>
<InlineFieldRow>
<InlineField label="Source" labelWidth={labelWidth} grow={true}>
<RadioButtonGroup value={mode} options={resourceOptions} onChange={onModeChange} fullWidth />
</InlineField>
</InlineFieldRow>
{mode !== ResourceDimensionMode.Fixed && (
<InlineFieldRow>
<InlineField label="Field" labelWidth={labelWidth} grow={true}>
<FieldNamePicker
context={context}
value={value.field ?? ''}
onChange={onFieldChange}
item={dummyFieldSettings}
/>
</InlineField>
</InlineFieldRow>
)}
{mode === ResourceDimensionMode.Fixed && (
<InlineFieldRow>
{resourceType === 'icon' && (
<InlineField label="Icon" labelWidth={labelWidth} grow={true}>
<IconSelector value={value?.fixed} onChange={onFixedChange} />
</InlineField>
)}
{resourceType === 'image' && (
<InlineField label="Image" labelWidth={labelWidth} grow={true}>
<StringValueEditor
context={context}
value={value?.fixed}
onChange={onFixedChange}
item={dummyImageStringSettings}
/>
</InlineField>
)}
</InlineFieldRow>
)}
{mode === ResourceDimensionMode.Mapping && (
<InlineFieldRow>
<InlineField label="Mappings" labelWidth={labelWidth} grow={true}>
<div>TODO mappings editor!</div>
</InlineField>
</InlineFieldRow>
)}
</>
);
};

View File

@ -5,8 +5,8 @@ import { InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
import {
useFieldDisplayNames,
useSelectOptions,
} from '../../../../../../../packages/grafana-ui/src/components/MatchersUI/utils';
import { NumberInput } from '../../components/NumberInput';
} from '../../../../../packages/grafana-ui/src/components/MatchersUI/utils';
import { NumberInput } from './NumberInput';
import { css } from '@emotion/css';
import { validateScaleOptions, validateScaleConfig } from '../scale';

View File

@ -0,0 +1,107 @@
import React, { FC, useCallback } from 'react';
import {
FieldNamePickerConfigSettings,
StandardEditorProps,
StandardEditorsRegistryItem,
StringFieldConfigSettings,
} from '@grafana/data';
import { TextDimensionConfig, TextDimensionMode, TextDimensionOptions } from '../types';
import { InlineField, InlineFieldRow, RadioButtonGroup, StringValueEditor } from '@grafana/ui';
import { FieldNamePicker } from '../../../../../packages/grafana-ui/src/components/MatchersUI/FieldNamePicker';
const textOptions = [
{ label: 'Fixed', value: TextDimensionMode.Fixed, description: 'Fixed value' },
{ label: 'Field', value: TextDimensionMode.Field, description: 'Display field value' },
{ label: 'Template', value: TextDimensionMode.Template, description: 'use template text' },
];
const dummyFieldSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
settings: {},
} as any;
const dummyStringSettings: StandardEditorsRegistryItem<string, StringFieldConfigSettings> = {
settings: {},
} as any;
export const TextDimensionEditor: FC<StandardEditorProps<TextDimensionConfig, TextDimensionOptions, any>> = (props) => {
const { value, context, onChange } = props;
const labelWidth = 9;
const onModeChange = useCallback(
(mode) => {
onChange({
...value,
mode,
});
},
[onChange, value]
);
const onFieldChange = useCallback(
(field) => {
onChange({
...value,
field,
});
},
[onChange, value]
);
const onFixedChange = useCallback(
(fixed) => {
onChange({
...value,
fixed,
});
},
[onChange, value]
);
const mode = value?.mode ?? TextDimensionMode.Fixed;
return (
<>
<InlineFieldRow>
<InlineField label="Source" labelWidth={labelWidth} grow={true}>
<RadioButtonGroup value={mode} options={textOptions} onChange={onModeChange} fullWidth />
</InlineField>
</InlineFieldRow>
{mode !== TextDimensionMode.Fixed && (
<InlineFieldRow>
<InlineField label="Field" labelWidth={labelWidth} grow={true}>
<FieldNamePicker
context={context}
value={value.field ?? ''}
onChange={onFieldChange}
item={dummyFieldSettings}
/>
</InlineField>
</InlineFieldRow>
)}
{mode === TextDimensionMode.Fixed && (
<InlineFieldRow>
<InlineField label={'Value'} labelWidth={labelWidth} grow={true}>
<StringValueEditor
context={context}
value={value?.fixed}
onChange={onFixedChange}
item={dummyStringSettings}
/>
</InlineField>
</InlineFieldRow>
)}
{mode === TextDimensionMode.Template && (
<InlineFieldRow>
<InlineField label="Template" labelWidth={labelWidth} grow={true}>
<StringValueEditor // This could be a code editor
context={context}
value={value?.fixed}
onChange={onFixedChange}
item={dummyStringSettings}
/>
</InlineField>
</InlineFieldRow>
)}
</>
);
};

View File

@ -0,0 +1,5 @@
export * from './ColorDimensionEditor';
export * from './IconSelector';
export * from './ResourceDimensionEditor';
export * from './ScaleDimensionEditor';
export * from './TextDimensionEditor';

View File

@ -0,0 +1,7 @@
export * from './types';
export * from './color';
export * from './scale';
export * from './text';
export * from './utils';
export * from './resource';

View File

@ -0,0 +1,49 @@
import { DataFrame } from '@grafana/data';
import { DimensionSupplier, ResourceDimensionConfig, ResourceDimensionMode } from './types';
import { findField, getLastNotNullFieldValue } from './utils';
//---------------------------------------------------------
// Resource dimension
//---------------------------------------------------------
export function getResourceDimension(
frame: DataFrame | undefined,
config: ResourceDimensionConfig
): DimensionSupplier<string> {
const mode = config.mode ?? ResourceDimensionMode.Fixed;
if (mode === ResourceDimensionMode.Fixed) {
const v = config.fixed!;
return {
isAssumed: !Boolean(v),
fixed: v,
value: () => v,
get: (i) => v,
};
}
const field = findField(frame, config.field);
if (!field) {
const v = '';
return {
isAssumed: true,
fixed: v,
value: () => v,
get: (i) => v,
};
}
if (mode === ResourceDimensionMode.Mapping) {
const mapper = (v: any) => `${v}`;
return {
field,
get: (i) => mapper(field.values.get(i)),
value: () => mapper(getLastNotNullFieldValue(field)),
};
}
return {
field,
get: field.values.get,
value: () => getLastNotNullFieldValue(field),
};
}

View File

@ -1,19 +1,23 @@
import { DataFrame } from '@grafana/data';
import { getMinMaxAndDelta } from '../../../../../../packages/grafana-data/src/field/scale';
import { getMinMaxAndDelta } from '../../../../packages/grafana-data/src/field/scale';
import { ScaleDimensionConfig, DimensionSupplier, ScaleDimensionOptions } from './types';
import { findField } from './utils';
import { findField, getLastNotNullFieldValue } from './utils';
//---------------------------------------------------------
// Scale dimension
//---------------------------------------------------------
export function getScaledDimension(frame: DataFrame, config: ScaleDimensionConfig): DimensionSupplier<number> {
export function getScaledDimension(
frame: DataFrame | undefined,
config: ScaleDimensionConfig
): DimensionSupplier<number> {
const field = findField(frame, config.field);
if (!field) {
const v = config.fixed ?? 0;
return {
isAssumed: Boolean(config.field?.length) || !config.fixed,
fixed: v,
value: () => v,
get: () => v,
};
}
@ -23,24 +27,28 @@ export function getScaledDimension(frame: DataFrame, config: ScaleDimensionConfi
if (values.length < 1 || delta <= 0 || info.delta <= 0) {
return {
fixed: config.min,
value: () => config.min,
get: () => config.min,
};
}
const get = (i: number) => {
const value = field.values.get(i);
let percent = 0;
if (value !== -Infinity) {
percent = (value - info.min!) / info.delta;
}
if (percent > 1) {
percent = 1;
} else if (percent < 0) {
percent = 0;
}
return config.min + percent * delta;
};
return {
get: (i) => {
const value = field.values.get(i);
let percent = 0;
if (value !== -Infinity) {
percent = (value - info.min!) / info.delta;
}
if (percent > 1) {
percent = 1;
} else if (percent < 0) {
percent = 0;
}
return config.min + percent * delta;
},
get,
value: () => get(getLastNotNullFieldValue(field)),
field,
};
}

View File

@ -0,0 +1,57 @@
import { DataFrame, formattedValueToString } from '@grafana/data';
import { DimensionSupplier, TextDimensionConfig, TextDimensionMode } from './types';
import { findField, getLastNotNullFieldValue } from './utils';
//---------------------------------------------------------
// Resource dimension
//---------------------------------------------------------
export function getTextDimension(frame: DataFrame | undefined, config: TextDimensionConfig): DimensionSupplier<string> {
let v = config.fixed;
const mode = config.mode ?? TextDimensionMode.Fixed;
if (mode === TextDimensionMode.Fixed) {
return {
isAssumed: !Boolean(v),
fixed: v,
value: () => v,
get: (i) => v,
};
}
const field = findField(frame, config.field);
if (mode === TextDimensionMode.Template) {
const disp = (v: any) => {
return `TEMPLATE[${config.fixed} // ${v}]`;
};
if (!field) {
v = disp('');
return {
isAssumed: true,
fixed: v,
value: () => v,
get: (i) => v,
};
}
return {
field,
get: (i) => disp(field.values.get(i)),
value: () => disp(getLastNotNullFieldValue(field)),
};
}
if (!field) {
return {
isAssumed: true,
fixed: v,
value: () => v,
get: (i) => v,
};
}
let disp = (v: any) => formattedValueToString(field.display!(v));
return {
field,
get: (i) => disp(field.values.get(i)),
value: () => disp(getLastNotNullFieldValue(field)),
};
}

View File

@ -17,10 +17,15 @@ export interface DimensionSupplier<T = any> {
field?: Field;
/**
* Explicit value -- if == null, then need a value pr index
* Explicit value -- if == null, then need a value for each index
*/
fixed?: T;
/**
* A single value -- typically last
*/
value: () => T;
/**
* Supplier for the dimension value
*/
@ -41,5 +46,36 @@ export interface ScaleDimensionOptions {
hideRange?: boolean; // false
}
export interface TextDimensionOptions {
// anything?
}
export enum TextDimensionMode {
Fixed = 'fixed',
Field = 'field',
Template = 'template',
}
export interface TextDimensionConfig extends BaseDimensionConfig<string> {
mode: TextDimensionMode;
}
/** Use the color value from field configs */
export interface ColorDimensionConfig extends BaseDimensionConfig<string> {}
/** Places that use the value */
export interface ResourceDimensionOptions {
resourceType: 'icon' | 'image';
}
export enum ResourceDimensionMode {
Fixed = 'fixed',
Field = 'field',
Mapping = 'mapping',
// pattern? uses field in the pattern
}
/** Get the path to a resource (URL) */
export interface ResourceDimensionConfig extends BaseDimensionConfig<string> {
mode: ResourceDimensionMode;
}

View File

@ -0,0 +1,38 @@
import { DataFrame, Field, getFieldDisplayName, ReducerID } from '@grafana/data';
export function findField(frame?: DataFrame, name?: string): Field | undefined {
if (!frame || !name?.length) {
return undefined;
}
for (const field of frame.fields) {
if (name === field.name) {
return field;
}
const disp = getFieldDisplayName(field, frame);
if (name === disp) {
return field;
}
}
return undefined;
}
export function getLastNotNullFieldValue<T>(field: Field): T {
const calcs = field.state?.calcs;
if (calcs) {
const v = calcs[ReducerID.lastNotNull];
if (v != null) {
return v as T;
}
}
const data = field.values;
let idx = data.length - 1;
while (idx >= 0) {
const v = data.get(idx--);
if (v != null) {
return v;
}
}
return undefined as any;
}

View File

@ -1,18 +0,0 @@
import { DataFrame, Field, getFieldDisplayName } from '@grafana/data';
export function findField(frame: DataFrame, name?: string): Field | undefined {
if (!name?.length) {
return undefined;
}
for (const field of frame.fields) {
if (name === field.name) {
return field;
}
const disp = getFieldDisplayName(field, frame);
if (name === disp) {
return field;
}
}
return undefined;
}

View File

@ -3,7 +3,7 @@ import { StandardEditorProps, SelectableValue } from '@grafana/data';
import { Button, InlineField, InlineFieldRow, Select, VerticalGroup } from '@grafana/ui';
import { GeomapPanelOptions, MapViewConfig } from '../types';
import { centerPointRegistry, MapCenterID } from '../view';
import { NumberInput } from '../components/NumberInput';
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
import { lastGeomapPanelInstance } from '../GeomapPanel';
import { toLonLat } from 'ol/proj';

View File

@ -3,8 +3,8 @@ import { Label, stylesFactory } from '@grafana/ui';
import { formattedValueToString, getFieldColorModeForField, GrafanaTheme } from '@grafana/data';
import { css } from '@emotion/css';
import { config } from 'app/core/config';
import { DimensionSupplier } from '../../dims/types';
import { getMinMaxAndDelta } from '../../../../../../../packages/grafana-data/src/field/scale';
import { DimensionSupplier } from 'app/features/dimensions';
import { getMinMaxAndDelta } from '../../../../../../../packages/grafana-data/src/field/scale';
export interface MarkersLegendProps {
color?: DimensionSupplier<string>;

View File

@ -11,9 +11,8 @@ import Feature from 'ol/Feature';
import * as layer from 'ol/layer';
import * as source from 'ol/source';
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
import { ScaleDimensionConfig, } from '../../dims/types';
import { ScaleDimensionEditor } from '../../dims/editors/ScaleDimensionEditor';
import { getScaledDimension } from '../../dims/scale';
import { ScaleDimensionConfig, getScaledDimension } from 'app/features/dimensions';
import { ScaleDimensionEditor } from 'app/features/dimensions/editors';
// Configuration options for Heatmap overlays
export interface HeatmapConfig {

View File

@ -1,3 +1,5 @@
import React, { ReactNode } from 'react';
import { MapLayerRegistryItem, MapLayerOptions, PanelData, GrafanaTheme2, FrameGeometrySourceMode } from '@grafana/data';
import Map from 'ol/Map';
import Feature from 'ol/Feature';
@ -6,17 +8,12 @@ import * as source from 'ol/source';
import tinycolor from 'tinycolor2';
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
import { ColorDimensionConfig, ScaleDimensionConfig, } from '../../dims/types';
import { getScaledDimension, } from '../../dims/scale';
import { getColorDimension, } from '../../dims/color';
import { ScaleDimensionEditor } from '../../dims/editors/ScaleDimensionEditor';
import { ColorDimensionEditor } from '../../dims/editors/ColorDimensionEditor';
import React from 'react';
import { ColorDimensionConfig, ScaleDimensionConfig, getScaledDimension, getColorDimension } from 'app/features/dimensions';
import { ScaleDimensionEditor, ColorDimensionEditor } from 'app/features/dimensions/editors';
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
import { ReplaySubject } from 'rxjs';
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
import { ReactNode } from 'react';
import { circleMarker, markerMakers } from '../../utils/regularShapes';
import { ReplaySubject } from 'rxjs';
// Configuration options for Circle overlays
export interface MarkersConfig {