diff --git a/x-pack/plugins/lens/public/assets/globe_illustration.tsx b/x-pack/plugins/lens/public/assets/globe_illustration.tsx new file mode 100644 index 0000000000000..af2f2c7a48e46 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/globe_illustration.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const GlobeIllustration = ({ title, titleId, ...props }: Omit) => ( + + + + + +); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 3296ddac7bc29..9bf03025e400f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -15,6 +15,7 @@ import { Action } from './state_management'; import { DragContext, DragDropIdentifier } from '../../drag_drop'; import { StateSetter, FramePublicAPI, DatasourceDataPanelProps, Datasource } from '../../types'; import { Query, Filter } from '../../../../../../src/plugins/data/public'; +import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; interface DataPanelWrapperProps { datasourceState: unknown; @@ -29,6 +30,7 @@ interface DataPanelWrapperProps { filters: Filter[]; dropOntoWorkspace: (field: DragDropIdentifier) => void; hasSuggestionForField: (field: DragDropIdentifier) => boolean; + plugins: { uiActions: UiActionsStart }; } export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { @@ -56,6 +58,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { showNoDataPopover: props.showNoDataPopover, dropOntoWorkspace: props.dropOntoWorkspace, hasSuggestionForField: props.hasSuggestionForField, + uiActions: props.plugins.uiActions, }; const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 362787ea91c4f..91b59664ada83 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -332,6 +332,7 @@ export function EditorFrame(props: EditorFrameProps) { showNoDataPopover={props.showNoDataPopover} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} + plugins={props.plugins} /> } configPanel={ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.scss new file mode 100644 index 0000000000000..ca1a62cae64d6 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.scss @@ -0,0 +1,18 @@ +@import '../../../mixins'; + +.lnsVisualizeGeoFieldWorkspacePanel__dragDrop { + padding: $euiSizeXXL ($euiSizeXL * 2); + border: $euiBorderThin; + border-radius: $euiBorderRadius; + + &.lnsDragDrop-isDropTarget { + @include lnsDroppable; + @include lnsDroppableActive; + + } + + &.lnsDragDrop-isActiveDropTarget { + @include lnsDroppableActiveHover; + + } +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.tsx new file mode 100644 index 0000000000000..b50b6463c5a25 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiPageContentBody, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + UiActionsStart, + VISUALIZE_GEO_FIELD_TRIGGER, +} from '../../../../../../../src/plugins/ui_actions/public'; +import { getVisualizeGeoFieldMessage } from '../../../utils'; +import { DragDrop } from '../../../drag_drop'; +import { GlobeIllustration } from '../../../assets/globe_illustration'; +import './geo_field_workspace_panel.scss'; + +interface Props { + fieldType: string; + fieldName: string; + indexPatternId: string; + uiActions: UiActionsStart; +} + +const dragDropIdentifier = { + id: 'lnsGeoFieldWorkspace', + humanData: { + label: i18n.translate('xpack.lens.geoFieldWorkspace.dropZoneLabel', { + defaultMessage: 'drop zone to open in maps', + }), + }, +}; + +const dragDropOrder = [1, 0, 0, 0]; + +export function GeoFieldWorkspacePanel(props: Props) { + function onDrop() { + props.uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER).exec({ + indexPatternId: props.indexPatternId, + fieldName: props.fieldName, + }); + } + + return ( + + +

+ {getVisualizeGeoFieldMessage(props.fieldType)} +

+ + +

+ + + +

+
+
+
+ ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index a31146e500434..3d5d9a6d84d81 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -338,17 +338,22 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ ); }; - return ( - + const dragDropContext = useContext(DragContext); + + const renderDragDrop = () => { + const customWorkspaceRenderer = + activeDatasourceId && + datasourceMap[activeDatasourceId]?.getCustomWorkspaceRenderer && + dragDropContext.dragging + ? datasourceMap[activeDatasourceId].getCustomWorkspaceRenderer!( + datasourceStates[activeDatasourceId].state, + dragDropContext.dragging + ) + : undefined; + + return customWorkspaceRenderer ? ( + customWorkspaceRenderer() + ) : ( + ); + }; + + return ( + + {renderDragDrop()} ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 849baa93652cc..46dc326a015a8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -44,7 +44,7 @@ export interface EditorFrameStartPlugins { embeddable?: EmbeddableStart; dashboard?: DashboardStart; expressions: ExpressionsStart; - uiActions?: UiActionsStart; + uiActions: UiActionsStart; charts: ChartsPluginSetup; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 6c5116436dddb..eeec7871a262c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -22,6 +22,7 @@ import { documentField } from './document_field'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { indexPatternFieldEditorPluginMock } from '../../../../../src/plugins/index_pattern_field_editor/public/mocks'; import { getFieldByNameFactory } from './pure_helpers'; +import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; const fieldsOne = [ { @@ -267,6 +268,7 @@ describe('IndexPattern Data Panel', () => { showNoDataPopover: jest.fn(), dropOntoWorkspace: jest.fn(), hasSuggestionForField: jest.fn(() => false), + uiActions: uiActionsPluginMock.createStartContract(), }; }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 4839d9388253b..a0a6b30e541a7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -41,6 +41,7 @@ import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; import { esQuery, IIndexPattern } from '../../../../../src/plugins/data/public'; import { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; +import { VISUALIZE_GEO_FIELD_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; export type Props = Omit, 'core'> & { data: DataPublicPluginStart; @@ -73,6 +74,8 @@ const supportedFieldTypes = new Set([ 'ip_range', 'histogram', 'document', + 'geo_point', + 'geo_shape', ]); const fieldTypeNames: Record = { @@ -83,6 +86,8 @@ const fieldTypeNames: Record = { date: i18n.translate('xpack.lens.datatypes.date', { defaultMessage: 'date' }), ip: i18n.translate('xpack.lens.datatypes.ipAddress', { defaultMessage: 'IP' }), histogram: i18n.translate('xpack.lens.datatypes.histogram', { defaultMessage: 'histogram' }), + geo_point: i18n.translate('xpack.lens.datatypes.geoPoint', { defaultMessage: 'geo_point' }), + geo_shape: i18n.translate('xpack.lens.datatypes.geoShape', { defaultMessage: 'geo_shape' }), }; // Wrapper around esQuery.buildEsQuery, handling errors (e.g. because a query can't be parsed) by @@ -121,6 +126,7 @@ export function IndexPatternDataPanel({ showNoDataPopover, dropOntoWorkspace, hasSuggestionForField, + uiActions, }: Props) { const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state; const onChangeIndexPattern = useCallback( @@ -233,6 +239,7 @@ export function IndexPatternDataPanel({ existenceFetchTimeout={state.existenceFetchTimeout} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} + uiActions={uiActions} /> )} @@ -286,6 +293,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ charts, dropOntoWorkspace, hasSuggestionForField, + uiActions, }: Omit & { data: DataPublicPluginStart; core: CoreStart; @@ -310,7 +318,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ isMetaAccordionOpen: false, }); const currentIndexPattern = indexPatterns[currentIndexPatternId]; - const allFields = currentIndexPattern.fields; + const visualizeGeoFieldTrigger = uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER); + const allFields = visualizeGeoFieldTrigger + ? currentIndexPattern.fields + : currentIndexPattern.fields.filter(({ type }) => type !== 'geo_point' && type !== 'geo_shape'); const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] })); const hasSyncedExistingFields = existingFields[currentIndexPattern.title]; const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter( @@ -807,6 +818,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ hasSuggestionForField={hasSuggestionForField} editField={editField} removeField={removeField} + uiActions={uiActions} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index dcc11ea426117..cf9f7c0c559e4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -17,6 +17,7 @@ import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { IndexPattern } from './types'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { documentField } from './document_field'; +import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; const chartsThemeService = chartPluginMock.createSetupContract().theme; @@ -109,6 +110,7 @@ describe('IndexPattern Field Item', () => { itemIndex: 0, dropOntoWorkspace: () => {}, hasSuggestionForField: () => false, + uiActions: uiActionsPluginMock.createStartContract(), }; data.fieldFormats = ({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index fce4fcda14cfc..013bb46500d0d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -54,6 +54,9 @@ import { BucketedAggregation, FieldStatsResponse } from '../../common'; import { IndexPattern, IndexPatternField, DraggedField } from './types'; import { LensFieldIcon } from './lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { VisualizeGeoFieldButton } from './visualize_geo_field_button'; +import { getVisualizeGeoFieldMessage } from '../utils'; import { debouncedComponent } from '../debounced_component'; @@ -75,6 +78,7 @@ export interface FieldItemProps { editField?: (name: string) => void; removeField?: (name: string) => void; hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; + uiActions: UiActionsStart; } interface State { @@ -149,7 +153,13 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { function fetchData() { // Range types don't have any useful stats we can show - if (state.isLoading || field.type === 'document' || field.type.includes('range')) { + if ( + state.isLoading || + field.type === 'document' || + field.type.includes('range') || + field.type === 'geo_point' || + field.type === 'geo_shape' + ) { return; } @@ -392,6 +402,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { removeField, hasSuggestionForField, hideDetails, + uiActions, } = props; const chartTheme = chartsThemeService.useChartsTheme(); @@ -467,6 +478,21 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { ); + } else if (field.type === 'geo_point' || field.type === 'geo_shape') { + return ( + <> + {panelHeader} + + {getVisualizeGeoFieldMessage(field.type)} + + + + + ); } else if ( (!props.histogram || props.histogram.buckets.length === 0) && (!props.topValues || props.topValues.buckets.length === 0) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx index ee0011ad0390c..13d5d25bc2ea2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -14,6 +14,7 @@ import { NoFieldsCallout } from './no_fields_callout'; import { IndexPatternField } from './types'; import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion'; import { DatasourceDataPanelProps } from '../types'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; const PAGINATION_SIZE = 50; export type FieldGroups = Record< @@ -55,6 +56,7 @@ export const FieldList = React.memo(function FieldList({ hasSuggestionForField, editField, removeField, + uiActions, }: { exists: (field: IndexPatternField) => boolean; fieldGroups: FieldGroups; @@ -72,6 +74,7 @@ export const FieldList = React.memo(function FieldList({ hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; editField?: (name: string) => void; removeField?: (name: string) => void; + uiActions: UiActionsStart; }) { const [pageSize, setPageSize] = useState(PAGINATION_SIZE); const [scrollContainer, setScrollContainer] = useState(undefined); @@ -155,6 +158,7 @@ export const FieldList = React.memo(function FieldList({ groupIndex={0} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} + uiActions={uiActions} /> )) )} @@ -206,6 +210,7 @@ export const FieldList = React.memo(function FieldList({ defaultNoFieldsMessage={fieldGroup.defaultNoFieldsMessage} /> } + uiActions={uiActions} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx index c8c48e2accf9b..6270b94abf565 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx @@ -15,6 +15,7 @@ import { IndexPattern } from './types'; import { FieldItem } from './field_item'; import { FieldsAccordion, FieldsAccordionProps, FieldItemSharedProps } from './fields_accordion'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; describe('Fields Accordion', () => { let defaultProps: FieldsAccordionProps; @@ -76,6 +77,7 @@ describe('Fields Accordion', () => { groupIndex: 0, dropOntoWorkspace: () => {}, hasSuggestionForField: () => false, + uiActions: uiActionsPluginMock.createStartContract(), }; }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index 42de7cb328b13..9f5409f9837f4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -24,6 +24,7 @@ import { Query, Filter } from '../../../../../src/plugins/data/public'; import { DatasourceDataPanelProps } from '../types'; import { IndexPattern } from './types'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; export interface FieldItemSharedProps { core: DatasourceDataPanelProps['core']; @@ -57,6 +58,7 @@ export interface FieldsAccordionProps { hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField']; editField?: (name: string) => void; removeField?: (name: string) => void; + uiActions: UiActionsStart; } export const FieldsAccordion = memo(function InnerFieldsAccordion({ @@ -80,6 +82,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ hasSuggestionForField, editField, removeField, + uiActions, }: FieldsAccordionProps) { const renderField = useCallback( (field: IndexPatternField, index) => ( @@ -95,6 +98,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ hasSuggestionForField={hasSuggestionForField} editField={editField} removeField={removeField} + uiActions={uiActions} /> ), [ @@ -106,6 +110,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({ groupIndex, editField, removeField, + uiActions, ] ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index a556c6ce0c095..f8bc84643bcab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -15,6 +15,7 @@ import { DataPublicPluginStart, } from '../../../../../src/plugins/data/public'; import { Datasource, EditorFrameSetup } from '../types'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; export interface IndexPatternDatasourceSetupPlugins { expressions: ExpressionsSetup; @@ -26,6 +27,7 @@ export interface IndexPatternDatasourceSetupPlugins { export interface IndexPatternDatasourceStartPlugins { data: DataPublicPluginStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; + uiActions: UiActionsStart; } export class IndexPatternDatasource { @@ -44,20 +46,23 @@ export class IndexPatternDatasource { getTimeScaleFunction, getSuffixFormatter, } = await import('../async_services'); - return core.getStartServices().then(([coreStart, { data, indexPatternFieldEditor }]) => { - data.fieldFormats.register([getSuffixFormatter(data.fieldFormats.deserialize)]); - expressions.registerFunction(getTimeScaleFunction(data)); - expressions.registerFunction(counterRate); - expressions.registerFunction(renameColumns); - expressions.registerFunction(formatColumn); - return getIndexPatternDatasource({ - core: coreStart, - storage: new Storage(localStorage), - data, - charts, - indexPatternFieldEditor, - }); - }) as Promise; + return core + .getStartServices() + .then(([coreStart, { data, indexPatternFieldEditor, uiActions }]) => { + data.fieldFormats.register([getSuffixFormatter(data.fieldFormats.deserialize)]); + expressions.registerFunction(getTimeScaleFunction(data)); + expressions.registerFunction(counterRate); + expressions.registerFunction(renameColumns); + expressions.registerFunction(formatColumn); + return getIndexPatternDatasource({ + core: coreStart, + storage: new Storage(localStorage), + data, + charts, + indexPatternFieldEditor, + uiActions, + }); + }) as Promise; }); } } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index c291c7ab3eac0..b1ff7b36b47a3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -17,6 +17,7 @@ import { getFieldByNameFactory } from './pure_helpers'; import { operationDefinitionMap, getErrorMessages } from './operations'; import { createMockedReferenceOperation } from './operations/mocks'; import { indexPatternFieldEditorPluginMock } from 'src/plugins/index_pattern_field_editor/public/mocks'; +import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; jest.mock('./loader'); jest.mock('../id_generator'); @@ -172,6 +173,7 @@ describe('IndexPattern Data Source', () => { data: dataPluginMock.createStartContract(), charts: chartPluginMock.createSetupContract(), indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), }); baseState = { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 81eb46e816715..8fb0994c42fb9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -45,7 +45,7 @@ import { import { isDraggedField, normalizeOperationDataType } from './utils'; import { LayerPanel } from './layerpanel'; import { IndexPatternColumn, getErrorMessages, IncompleteColumn } from './operations'; -import { IndexPatternPrivateState, IndexPatternPersistedState } from './types'; +import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public'; @@ -53,6 +53,9 @@ import { mergeLayer } from './state_helpers'; import { Datasource, StateSetter } from '../types'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { deleteColumn, isReferenced } from './operations'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { GeoFieldWorkspacePanel } from '../editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel'; +import { DraggingIdentifier } from '../drag_drop'; export { OperationType, IndexPatternColumn, deleteColumn } from './operations'; @@ -78,12 +81,14 @@ export function getIndexPatternDatasource({ data, charts, indexPatternFieldEditor, + uiActions, }: { core: CoreStart; storage: IStorageWrapper; data: DataPublicPluginStart; charts: ChartsPluginSetup; indexPatternFieldEditor: IndexPatternFieldEditorStart; + uiActions: UiActionsStart; }) { const uiSettings = core.uiSettings; const onIndexPatternLoadError = (err: Error) => @@ -197,6 +202,7 @@ export function getIndexPatternDatasource({ indexPatternFieldEditor={indexPatternFieldEditor} {...props} core={core} + uiActions={uiActions} /> , domElement @@ -320,6 +326,34 @@ export function getIndexPatternDatasource({ getDropProps, onDrop, + getCustomWorkspaceRenderer: (state: IndexPatternPrivateState, dragging: DraggingIdentifier) => { + if (dragging.field === undefined || dragging.indexPatternId === undefined) { + return undefined; + } + + const draggedField = dragging as DraggingIdentifier & { + field: IndexPatternField; + indexPatternId: string; + }; + const geoFieldType = + draggedField.field.esTypes && + draggedField.field.esTypes.find((esType) => { + return ['geo_point', 'geo_shape'].includes(esType); + }); + return geoFieldType + ? () => { + return ( + + ); + } + : undefined; + }, + // Reset the temporary invalid state when closing the editor, but don't // update the state if it's not needed updateStateOnCloseDimension: ({ state, layerId, columnId }) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/visualize_geo_field_button.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/visualize_geo_field_button.tsx new file mode 100644 index 0000000000000..3101fb12b933a --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/visualize_geo_field_button.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { MouseEvent, useEffect, useState } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + visualizeGeoFieldTrigger, + VISUALIZE_GEO_FIELD_TRIGGER, + UiActionsStart, +} from '../../../../../src/plugins/ui_actions/public'; + +interface Props { + indexPatternId: string; + fieldName: string; + uiActions: UiActionsStart; +} + +export function VisualizeGeoFieldButton(props: Props) { + const [href, setHref] = useState(undefined); + + async function loadHref() { + const actions = await props.uiActions.getTriggerCompatibleActions(VISUALIZE_GEO_FIELD_TRIGGER, { + indexPatternId: props.indexPatternId, + fieldName: props.fieldName, + }); + const triggerOptions = { + indexPatternId: props.indexPatternId, + fieldName: props.fieldName, + trigger: visualizeGeoFieldTrigger, + }; + const loadedHref = actions.length ? await actions[0].getHref?.(triggerOptions) : undefined; + setHref(loadedHref); + } + + useEffect( + () => { + loadHref(); + }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + [] + ); + + function onClick(event: MouseEvent) { + event.preventDefault(); + props.uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER).exec({ + indexPatternId: props.indexPatternId, + fieldName: props.fieldName, + }); + } + + return ( + <> + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + + + + ); +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 94b4433a82551..51d679e7c40e5 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -33,6 +33,7 @@ import type { LensResizeActionData, LensToggleActionData, } from './datatable_visualization/components/types'; +import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; export type ErrorCallback = (e: { message: string }) => void; @@ -213,6 +214,10 @@ export interface Datasource { } ) => { dropTypes: DropType[]; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; + getCustomWorkspaceRenderer?: ( + state: T, + dragging: DraggingIdentifier + ) => undefined | (() => JSX.Element); updateStateOnCloseDimension?: (props: { layerId: string; columnId: string; @@ -267,6 +272,7 @@ export interface DatasourceDataPanelProps { filters: Filter[]; dropOntoWorkspace: (field: DragDropIdentifier) => void; hasSuggestionForField: (field: DragDropIdentifier) => boolean; + uiActions: UiActionsStart; } interface SharedDimensionProps { @@ -343,7 +349,7 @@ export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProp dropType: DropType; }; -export type FieldOnlyDataType = 'document' | 'ip' | 'histogram'; +export type FieldOnlyDataType = 'document' | 'ip' | 'histogram' | 'geo_point' | 'geo_shape'; export type DataType = 'string' | 'number' | 'date' | 'boolean' | FieldOnlyDataType; // An operation represents a column in a table, not any information diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index ab95141431541..2d8cfee2185fa 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -42,3 +42,10 @@ export const desanitizeFilterContext = ( } return result; }; + +export function getVisualizeGeoFieldMessage(fieldType: string) { + return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { + defaultMessage: `Lens cannot visualize {fieldType} fields`, + values: { fieldType }, + }); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 1616a4c141476..d5120cfae973c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -27,6 +27,8 @@ const columnSortOrder = { boolean: 4, number: 5, histogram: 6, + geo_point: 7, + geo_shape: 8, }; /** diff --git a/x-pack/test/functional/apps/lens/geo_field.ts b/x-pack/test/functional/apps/lens/geo_field.ts new file mode 100644 index 0000000000000..2ba833177a135 --- /dev/null +++ b/x-pack/test/functional/apps/lens/geo_field.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens', 'header', 'maps', 'timePicker']); + + describe('lens visualize geo field tests', () => { + it('should visualize geo fields in maps', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 22, 2015 @ 00:00:00.000', + 'Sep 22, 2015 @ 04:00:00.000' + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToGeoFieldWorkspace('geo.coordinates'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + const doesLayerExist = await PageObjects.maps.doesLayerExist('logstash-*'); + expect(doesLayerExist).to.equal(true); + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('66'); + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index bfb0aad7177f4..ab7cee13ffebd 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -37,6 +37,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./colors')); loadTestFile(require.resolve('./chart_data')); loadTestFile(require.resolve('./drag_and_drop')); + loadTestFile(require.resolve('./geo_field')); loadTestFile(require.resolve('./lens_reporting')); loadTestFile(require.resolve('./lens_tagging')); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 100ed8e079d37..f73440e331466 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -174,6 +174,19 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.header.waitUntilLoadingHasFinished(); }, + /** + * Drags field to geo field workspace + * + * @param field - the desired geo_point or geo_shape field + * */ + async dragFieldToGeoFieldWorkspace(field: string) { + await browser.html5DragAndDrop( + testSubjects.getCssSelector(`lnsFieldListPanelField-${field}`), + testSubjects.getCssSelector('lnsGeoFieldWorkspace') + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }, + /** * Drags field to workspace *