[Maps] layer wizard select re-design (#69313)

* [Maps] layer wizard select re-design

* review feedback

* tslint

* add unit test

* use smaller gutters

* review feedback
This commit is contained in:
Nathan Reese 2020-06-17 16:17:30 -06:00 committed by GitHub
parent b434cac29a
commit f7266d3b7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 304 additions and 43 deletions

View file

@ -227,3 +227,9 @@ export enum INITIAL_LOCATION {
FIXED_LOCATION = 'FIXED_LOCATION',
BROWSER_LOCATION = 'BROWSER_LOCATION',
}
export enum LAYER_WIZARD_CATEGORY {
ELASTICSEARCH = 'ELASTICSEARCH',
REFERENCE = 'REFERENCE',
SOLUTIONS = 'SOLUTIONS',
}

View file

@ -7,6 +7,7 @@
import { ReactElement } from 'react';
import { LayerDescriptor } from '../../../common/descriptor_types';
import { LAYER_WIZARD_CATEGORY } from '../../../common/constants';
export type RenderWizardArguments = {
previewLayers: (layerDescriptors: LayerDescriptor[], isIndexingSource?: boolean) => void;
@ -20,6 +21,7 @@ export type RenderWizardArguments = {
};
export type LayerWizard = {
categories: LAYER_WIZARD_CATEGORY[];
checkVisibility?: () => Promise<boolean>;
description: string;
icon: string;

View file

@ -6,12 +6,14 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { LAYER_WIZARD_CATEGORY } from '../../../../../common/constants';
import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry';
import { ObservabilityLayerTemplate } from './observability_layer_template';
import { APM_INDEX_PATTERN_ID } from './create_layer_descriptor';
import { getIndexPatternService } from '../../../../kibana_services';
export const ObservabilityLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH, LAYER_WIZARD_CATEGORY.SOLUTIONS],
checkVisibility: async () => {
try {
await getIndexPatternService().get(APM_INDEX_PATTERN_ID);

View file

@ -6,11 +6,13 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { LAYER_WIZARD_CATEGORY } from '../../../../../common/constants';
import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry';
import { getSecurityIndexPatterns } from './security_index_pattern_utils';
import { SecurityLayerTemplate } from './security_layer_template';
export const SecurityLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH, LAYER_WIZARD_CATEGORY.SOLUTIONS],
checkVisibility: async () => {
const indexPatterns = await getSecurityIndexPatterns();
return indexPatterns.length > 0;

View file

@ -22,6 +22,7 @@ import { GeojsonFileSource } from './geojson_file_source';
import { VectorLayer } from '../../layers/vector_layer/vector_layer';
export const uploadLayerWizardConfig: LayerWizard = {
categories: [],
description: i18n.translate('xpack.maps.source.geojsonFileDescription', {
defaultMessage: 'Index GeoJSON data in Elasticsearch',
}),

View file

@ -13,8 +13,10 @@ import { EMSFileSource, sourceTitle } from './ems_file_source';
// @ts-ignore
import { getIsEmsEnabled } from '../../../kibana_services';
import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
export const emsBoundariesLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
checkVisibility: () => {
return getIsEmsEnabled();
},

View file

@ -13,8 +13,10 @@ import { VectorTileLayer } from '../../layers/vector_tile_layer/vector_tile_laye
// @ts-ignore
import { TileServiceSelect } from './tile_service_select';
import { getIsEmsEnabled } from '../../../kibana_services';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
export const emsBaseMapLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
checkVisibility: () => {
return getIsEmsEnabled();
},

View file

@ -23,6 +23,7 @@ import {
COUNT_PROP_NAME,
COLOR_MAP_TYPE,
FIELD_ORIGIN,
LAYER_WIZARD_CATEGORY,
RENDER_AS,
VECTOR_STYLES,
STYLE_TYPE,
@ -30,6 +31,7 @@ import {
import { COLOR_GRADIENTS } from '../../styles/color_utils';
export const clustersLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
description: i18n.translate('xpack.maps.source.esGridClustersDescription', {
defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell',
}),

View file

@ -14,9 +14,10 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re
// @ts-ignore
import { HeatmapLayer } from '../../layers/heatmap_layer/heatmap_layer';
import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types';
import { RENDER_AS } from '../../../../common/constants';
import { LAYER_WIZARD_CATEGORY, RENDER_AS } from '../../../../common/constants';
export const heatmapLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
description: i18n.translate('xpack.maps.source.esGridHeatmapDescription', {
defaultMessage: 'Geospatial data grouped in grids to show density',
}),

View file

@ -14,6 +14,7 @@ import { VectorStyle } from '../../styles/vector/vector_style';
import {
FIELD_ORIGIN,
COUNT_PROP_NAME,
LAYER_WIZARD_CATEGORY,
VECTOR_STYLES,
STYLE_TYPE,
} from '../../../../common/constants';
@ -24,6 +25,7 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re
import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/descriptor_types';
export const point2PointLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
description: i18n.translate('xpack.maps.source.pewPewDescription', {
defaultMessage: 'Aggregated data paths between the source and destination',
}),

View file

@ -13,7 +13,7 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re
import { ESSearchSource, sourceTitle } from './es_search_source';
import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer';
import { VectorLayer } from '../../layers/vector_layer/vector_layer';
import { SCALING_TYPES } from '../../../../common/constants';
import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants';
export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: string[]) {
const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig);
@ -24,6 +24,7 @@ export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: s
}
export const esDocumentsLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
description: i18n.translate('xpack.maps.source.esSearchDescription', {
defaultMessage: 'Vector data from a Kibana index pattern',
}),

View file

@ -13,8 +13,10 @@ import { VectorLayer } from '../../layers/vector_layer/vector_layer';
// @ts-ignore
import { CreateSourceEditor } from './create_source_editor';
import { getKibanaRegionList } from '../../../meta';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
export const kibanaRegionMapLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
checkVisibility: async () => {
const regions = getKibanaRegionList();
return regions.length > 0;

View file

@ -13,8 +13,10 @@ import { CreateSourceEditor } from './create_source_editor';
import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source';
import { TileLayer } from '../../layers/tile_layer/tile_layer';
import { getKibanaTileMap } from '../../../meta';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
export const kibanaBasemapLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
checkVisibility: async () => {
const tilemap = getKibanaTileMap();
// @ts-ignore

View file

@ -13,8 +13,10 @@ import {
import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source';
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
export const mvtVectorSourceWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', {
defaultMessage: 'Vector source wizard',
}),

View file

@ -12,8 +12,10 @@ import { WMSCreateSourceEditor } from './wms_create_source_editor';
import { sourceTitle, WMSSource } from './wms_source';
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
import { TileLayer } from '../../layers/tile_layer/tile_layer';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
export const wmsLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
description: i18n.translate('xpack.maps.source.wmsDescription', {
defaultMessage: 'Maps from OGC Standard WMS',
}),

View file

@ -10,8 +10,10 @@ import { XYZTMSEditor, XYZTMSSourceConfig } from './xyz_tms_editor';
import { XYZTMSSource, sourceTitle } from './xyz_tms_source';
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
import { TileLayer } from '../../layers/tile_layer/tile_layer';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
export const tmsLayerWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
description: i18n.translate('xpack.maps.source.ems_xyzDescription', {
defaultMessage: 'Tile map service configured in interface',
}),

View file

@ -1,5 +1,4 @@
@import 'gis_map/gis_map';
@import 'add_layer_panel/index';
@import 'layer_panel/index';
@import 'widget_overlay/index';
@import 'toolbar_overlay/index';

View file

@ -1,12 +0,0 @@
.mapLayerAddpanel__card {
// EUITODO: Fix horizontal layout so it works with any size icon
.euiCard__content {
// sass-lint:disable-block no-important
padding-top: 0 !important;
}
.euiCard__top + .euiCard__content {
// sass-lint:disable-block no-important
padding-top: 2px !important;
}
}

View file

@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LayerWizardSelect Should render layer select after layer wizards are loaded 1`] = `
<Fragment>
<EuiFacetGroup
layout="horizontal"
>
<EuiFacetButton
isSelected={true}
key="all"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="All"
id="xpack.maps.layerWizardSelect.allCategories"
values={Object {}}
/>
</EuiFacetButton>
<EuiFacetButton
isSelected={false}
key="ELASTICSEARCH"
onClick={[Function]}
>
Elasticsearch
</EuiFacetButton>
<EuiFacetButton
isSelected={false}
key="SOLUTIONS"
onClick={[Function]}
>
Solutions
</EuiFacetButton>
</EuiFacetGroup>
<EuiSpacer
size="s"
/>
<EuiFlexGrid
columns={2}
gutterSize="m"
>
<EuiFlexItem
key="wizard 1"
>
<EuiCard
data-test-subj="wizard1"
description="mock wizard without icon"
onClick={[Function]}
title="wizard 1"
/>
</EuiFlexItem>
<EuiFlexItem
key="wizard 2"
>
<EuiCard
data-test-subj="wizard2"
description="mock wizard with icon"
icon={
<EuiIcon
size="l"
type="logoObservability"
/>
}
onClick={[Function]}
title="wizard 2"
/>
</EuiFlexItem>
</EuiFlexGrid>
</Fragment>
`;
exports[`LayerWizardSelect Should render loading screen before layer wizards are loaded 1`] = `
<div>
<EuiCard
description={
<EuiLoadingContent
lines={2}
/>
}
layout="horizontal"
title=""
/>
</div>
`;

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('../../../classes/layers/layer_wizard_registry', () => ({}));
import React from 'react';
import { shallow } from 'enzyme';
import { LayerWizardSelect } from './layer_wizard_select';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
const defaultProps = {
onSelect: () => {},
};
describe('LayerWizardSelect', () => {
beforeAll(() => {
require('../../../classes/layers/layer_wizard_registry').getLayerWizards = async () => {
return [
{
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
description: 'mock wizard without icon',
renderWizard: () => {
return <div />;
},
title: 'wizard 1',
},
{
categories: [LAYER_WIZARD_CATEGORY.SOLUTIONS],
description: 'mock wizard with icon',
icon: 'logoObservability',
renderWizard: () => {
return <div />;
},
title: 'wizard 2',
},
];
};
});
test('Should render layer select after layer wizards are loaded', async () => {
const component = shallow(<LayerWizardSelect {...defaultProps} />);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render loading screen before layer wizards are loaded', () => {
const component = shallow(<LayerWizardSelect {...defaultProps} />);
expect(component).toMatchSnapshot();
});
});

View file

@ -5,26 +5,63 @@
*/
import _ from 'lodash';
import React, { Component, Fragment } from 'react';
import { EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui';
import { EuiLoadingContent } from '@elastic/eui';
import React, { Component } from 'react';
import {
EuiCard,
EuiIcon,
EuiFlexGrid,
EuiFlexItem,
EuiLoadingContent,
EuiFacetGroup,
EuiFacetButton,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { getLayerWizards, LayerWizard } from '../../../classes/layers/layer_wizard_registry';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
interface Props {
onSelect: (layerWizard: LayerWizard) => void;
}
interface State {
layerWizards: LayerWizard[];
activeCategories: LAYER_WIZARD_CATEGORY[];
hasLoadedWizards: boolean;
layerWizards: LayerWizard[];
selectedCategory: LAYER_WIZARD_CATEGORY | null;
}
function getCategoryLabel(category: LAYER_WIZARD_CATEGORY): string {
if (category === LAYER_WIZARD_CATEGORY.ELASTICSEARCH) {
return i18n.translate('xpack.maps.layerWizardSelect.elasticsearchCategoryLabel', {
defaultMessage: 'Elasticsearch',
});
}
if (category === LAYER_WIZARD_CATEGORY.REFERENCE) {
return i18n.translate('xpack.maps.layerWizardSelect.referenceCategoryLabel', {
defaultMessage: 'Reference',
});
}
if (category === LAYER_WIZARD_CATEGORY.SOLUTIONS) {
return i18n.translate('xpack.maps.layerWizardSelect.solutionsCategoryLabel', {
defaultMessage: 'Solutions',
});
}
throw new Error(`Unexpected category: ${category}`);
}
export class LayerWizardSelect extends Component<Props, State> {
private _isMounted: boolean = false;
state = {
layerWizards: [],
activeCategories: [],
hasLoadedWizards: false,
layerWizards: [],
selectedCategory: null,
};
componentDidMount() {
@ -38,11 +75,59 @@ export class LayerWizardSelect extends Component<Props, State> {
async _loadLayerWizards() {
const layerWizards = await getLayerWizards();
const activeCategories: LAYER_WIZARD_CATEGORY[] = [];
layerWizards.forEach((layerWizard: LayerWizard) => {
layerWizard.categories.forEach((category: LAYER_WIZARD_CATEGORY) => {
if (!activeCategories.includes(category)) {
activeCategories.push(category);
}
});
});
if (this._isMounted) {
this.setState({ layerWizards, hasLoadedWizards: true });
this.setState({
activeCategories,
layerWizards,
hasLoadedWizards: true,
});
}
}
_filterByCategory(category: LAYER_WIZARD_CATEGORY | null) {
this.setState({ selectedCategory: category });
}
_renderCategoryFacets() {
if (this.state.activeCategories.length === 0) {
return null;
}
const facets = this.state.activeCategories.map((category: LAYER_WIZARD_CATEGORY) => {
return (
<EuiFacetButton
key={category}
isSelected={category === this.state.selectedCategory}
onClick={() => this._filterByCategory(category)}
>
{getCategoryLabel(category)}
</EuiFacetButton>
);
});
return (
<EuiFacetGroup layout="horizontal">
<EuiFacetButton
key="all"
isSelected={!this.state.selectedCategory}
onClick={() => this._filterByCategory(null)}
>
<FormattedMessage id="xpack.maps.layerWizardSelect.allCategories" defaultMessage="All" />
</EuiFacetButton>
{facets}
</EuiFacetGroup>
);
}
render() {
if (!this.state.hasLoadedWizards) {
return (
@ -51,27 +136,41 @@ export class LayerWizardSelect extends Component<Props, State> {
</div>
);
}
return this.state.layerWizards.map((layerWizard: LayerWizard) => {
const icon = layerWizard.icon ? <EuiIcon type={layerWizard.icon} size="l" /> : undefined;
const onClick = () => {
this.props.onSelect(layerWizard);
};
const wizardCards = this.state.layerWizards
.filter((layerWizard: LayerWizard) => {
return this.state.selectedCategory
? layerWizard.categories.includes(this.state.selectedCategory!)
: true;
})
.map((layerWizard: LayerWizard) => {
const icon = layerWizard.icon ? <EuiIcon type={layerWizard.icon} size="l" /> : undefined;
return (
<Fragment key={layerWizard.title}>
<EuiSpacer size="s" />
<EuiCard
className="mapLayerAddpanel__card"
title={layerWizard.title}
icon={icon}
onClick={onClick}
description={layerWizard.description}
layout="horizontal"
data-test-subj={_.camelCase(layerWizard.title)}
/>
</Fragment>
);
});
const onClick = () => {
this.props.onSelect(layerWizard);
};
return (
<EuiFlexItem key={layerWizard.title}>
<EuiCard
title={layerWizard.title}
icon={icon}
onClick={onClick}
description={layerWizard.description}
data-test-subj={_.camelCase(layerWizard.title)}
/>
</EuiFlexItem>
);
});
return (
<>
{this._renderCategoryFacets()}
<EuiSpacer size="s" />
<EuiFlexGrid columns={2} gutterSize="m">
{wizardCards}
</EuiFlexGrid>
</>
);
}
}

View file

@ -9,11 +9,11 @@
overflow: hidden;
> * {
width: $euiSizeXXL * 11;
width: $euiSizeXXL * 12;
}
&-isVisible {
width: $euiSizeXXL * 11;
width: $euiSizeXXL * 12;
transition: width $euiAnimSpeedNormal $euiAnimSlightResistance;
}
}