[Maps] add attribution to layer editor (#98328)
* [Maps] add attribution to layer editor * update sources to getAttributionProvider * remove attribution UI from EMS_XYZ source * remove attribution UI from WMS source editor * clean up * tslint * AttributionFormRow jest test * add migration * tslint * i18n fixes * attribution * [Maps] Improving design and a11y for attribution layer settings (#38) * Design and a11y improvements * Buttons aria labels * Addressing PR review * tslint and i18n fixes * update jest snapshots * remove placeholder for url Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elizabet Oliveira <elizabet.oliveira@elastic.co>
This commit is contained in:
parent
24fd3a1e18
commit
85b78711c6
|
@ -16,6 +16,11 @@ import {
|
|||
import { DataRequestDescriptor } from './data_request_descriptor_types';
|
||||
import { AbstractSourceDescriptor, TermJoinSourceDescriptor } from './source_descriptor_types';
|
||||
|
||||
export type Attribution = {
|
||||
label: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type JoinDescriptor = {
|
||||
leftField?: string;
|
||||
right: TermJoinSourceDescriptor;
|
||||
|
@ -29,6 +34,7 @@ export type LayerDescriptor = {
|
|||
__trackedLayerDescriptor?: LayerDescriptor;
|
||||
__areTilesLoaded?: boolean;
|
||||
alpha?: number;
|
||||
attribution?: Attribution;
|
||||
id: string;
|
||||
joins?: JoinDescriptor[];
|
||||
label?: string | null;
|
||||
|
|
|
@ -19,11 +19,6 @@ import {
|
|||
SOURCE_TYPES,
|
||||
} from '../constants';
|
||||
|
||||
export type AttributionDescriptor = {
|
||||
attributionText?: string;
|
||||
attributionUrl?: string;
|
||||
};
|
||||
|
||||
export type AbstractSourceDescriptor = {
|
||||
id?: string;
|
||||
type: string;
|
||||
|
@ -129,14 +124,11 @@ export type WMSSourceDescriptor = AbstractSourceDescriptor & {
|
|||
serviceUrl: string;
|
||||
layers: string;
|
||||
styles: string;
|
||||
attributionText: string;
|
||||
attributionUrl: string;
|
||||
};
|
||||
|
||||
export type XYZTMSSourceDescriptor = AbstractSourceDescriptor &
|
||||
AttributionDescriptor & {
|
||||
urlTemplate: string;
|
||||
};
|
||||
export type XYZTMSSourceDescriptor = AbstractSourceDescriptor & {
|
||||
urlTemplate: string;
|
||||
};
|
||||
|
||||
export type MVTFieldDescriptor = {
|
||||
name: string;
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { moveAttribution } from './move_attribution';
|
||||
import { LayerDescriptor } from '../descriptor_types';
|
||||
|
||||
test('Should handle missing layerListJSON attribute', () => {
|
||||
const attributes = {
|
||||
title: 'my map',
|
||||
};
|
||||
expect(moveAttribution({ attributes })).toEqual({
|
||||
title: 'my map',
|
||||
});
|
||||
});
|
||||
|
||||
test('Should migrate source attribution to layer attribution', () => {
|
||||
const layerListJSON = JSON.stringify(([
|
||||
{
|
||||
sourceDescriptor: {
|
||||
attributionText: 'myLabel',
|
||||
attributionUrl: 'myUrl',
|
||||
id: 'mySourceId',
|
||||
},
|
||||
},
|
||||
] as unknown) as LayerDescriptor[]);
|
||||
|
||||
const attributes = {
|
||||
title: 'my map',
|
||||
layerListJSON,
|
||||
};
|
||||
|
||||
const { layerListJSON: migratedLayerListJSON } = moveAttribution({ attributes });
|
||||
const migratedLayerList = JSON.parse(migratedLayerListJSON!);
|
||||
expect(migratedLayerList).toEqual([
|
||||
{
|
||||
attribution: { label: 'myLabel', url: 'myUrl' },
|
||||
sourceDescriptor: { id: 'mySourceId' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should not add attribution to layer when source does not provide attribution', () => {
|
||||
const layerListJSON = JSON.stringify(([
|
||||
{
|
||||
sourceDescriptor: {
|
||||
id: 'mySourceId',
|
||||
},
|
||||
},
|
||||
] as unknown) as LayerDescriptor[]);
|
||||
|
||||
const attributes = {
|
||||
title: 'my map',
|
||||
layerListJSON,
|
||||
};
|
||||
|
||||
const { layerListJSON: migratedLayerListJSON } = moveAttribution({ attributes });
|
||||
const migratedLayerList = JSON.parse(migratedLayerListJSON!);
|
||||
expect(migratedLayerList).toEqual([
|
||||
{
|
||||
sourceDescriptor: { id: 'mySourceId' },
|
||||
},
|
||||
]);
|
||||
});
|
42
x-pack/plugins/maps/common/migrations/move_attribution.ts
Normal file
42
x-pack/plugins/maps/common/migrations/move_attribution.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { MapSavedObjectAttributes } from '../map_saved_object_type';
|
||||
import { LayerDescriptor } from '../descriptor_types';
|
||||
|
||||
// In 7.14, attribution added to the layer_descriptor. Prior to 7.14, 2 sources, WMS and TMS, had attribution on source descriptor.
|
||||
export function moveAttribution({
|
||||
attributes,
|
||||
}: {
|
||||
attributes: MapSavedObjectAttributes;
|
||||
}): MapSavedObjectAttributes {
|
||||
if (!attributes || !attributes.layerListJSON) {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON);
|
||||
|
||||
layerList.forEach((layer: LayerDescriptor) => {
|
||||
const sourceDescriptor = layer.sourceDescriptor as {
|
||||
attributionText?: string;
|
||||
attributionUrl?: string;
|
||||
};
|
||||
if (sourceDescriptor.attributionText && sourceDescriptor.attributionUrl) {
|
||||
layer.attribution = {
|
||||
label: sourceDescriptor.attributionText,
|
||||
url: sourceDescriptor.attributionUrl,
|
||||
};
|
||||
delete sourceDescriptor.attributionText;
|
||||
delete sourceDescriptor.attributionUrl;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
layerListJSON: JSON.stringify(layerList),
|
||||
};
|
||||
}
|
|
@ -24,6 +24,7 @@ import { updateFlyout } from './ui_actions';
|
|||
import {
|
||||
ADD_LAYER,
|
||||
ADD_WAITING_FOR_MAP_READY_LAYER,
|
||||
CLEAR_LAYER_PROP,
|
||||
CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST,
|
||||
REMOVE_LAYER,
|
||||
REMOVE_TRACKED_LAYER_STATE,
|
||||
|
@ -40,7 +41,12 @@ import {
|
|||
} from './map_action_constants';
|
||||
import { clearDataRequests, syncDataForLayerId, updateStyleMeta } from './data_request_actions';
|
||||
import { cleanTooltipStateForLayer } from './tooltip_actions';
|
||||
import { JoinDescriptor, LayerDescriptor, StyleDescriptor } from '../../common/descriptor_types';
|
||||
import {
|
||||
Attribution,
|
||||
JoinDescriptor,
|
||||
LayerDescriptor,
|
||||
StyleDescriptor,
|
||||
} from '../../common/descriptor_types';
|
||||
import { ILayer } from '../classes/layers/layer';
|
||||
import { IVectorLayer } from '../classes/layers/vector_layer';
|
||||
import { LAYER_STYLE_TYPE, LAYER_TYPE } from '../../common/constants';
|
||||
|
@ -349,6 +355,23 @@ export function updateLayerLabel(id: string, newLabel: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export function setLayerAttribution(id: string, attribution: Attribution) {
|
||||
return {
|
||||
type: UPDATE_LAYER_PROP,
|
||||
id,
|
||||
propName: 'attribution',
|
||||
newValue: attribution,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearLayerAttribution(id: string) {
|
||||
return {
|
||||
type: CLEAR_LAYER_PROP,
|
||||
id,
|
||||
propName: 'attribution',
|
||||
};
|
||||
}
|
||||
|
||||
export function updateLayerMinZoom(id: string, minZoom: number) {
|
||||
return {
|
||||
type: UPDATE_LAYER_PROP,
|
||||
|
|
|
@ -10,6 +10,7 @@ export const UPDATE_LAYER_ORDER = 'UPDATE_LAYER_ORDER';
|
|||
export const ADD_LAYER = 'ADD_LAYER';
|
||||
export const SET_LAYER_ERROR_STATUS = 'SET_LAYER_ERROR_STATUS';
|
||||
export const ADD_WAITING_FOR_MAP_READY_LAYER = 'ADD_WAITING_FOR_MAP_READY_LAYER';
|
||||
export const CLEAR_LAYER_PROP = 'CLEAR_LAYER_PROP';
|
||||
export const CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST = 'CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST';
|
||||
export const REMOVE_LAYER = 'REMOVE_LAYER';
|
||||
export const SET_LAYER_VISIBILITY = 'SET_LAYER_VISIBILITY';
|
||||
|
|
|
@ -30,13 +30,14 @@ import {
|
|||
import { copyPersistentState } from '../../reducers/copy_persistent_state';
|
||||
import {
|
||||
AggDescriptor,
|
||||
Attribution,
|
||||
ESTermSourceDescriptor,
|
||||
JoinDescriptor,
|
||||
LayerDescriptor,
|
||||
MapExtent,
|
||||
StyleDescriptor,
|
||||
} from '../../../common/descriptor_types';
|
||||
import { Attribution, ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source';
|
||||
import { ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source';
|
||||
import { DataRequestContext } from '../../actions';
|
||||
import { IStyle } from '../styles/style';
|
||||
import { getJoinAggKey } from '../../../common/get_agg_key';
|
||||
|
@ -99,6 +100,7 @@ export interface ILayer {
|
|||
isFittable(): Promise<boolean>;
|
||||
getLicensedFeatures(): Promise<LICENSED_FEATURES[]>;
|
||||
getCustomIconAndTooltipContent(): CustomIconAndTooltipContent;
|
||||
getDescriptor(): LayerDescriptor;
|
||||
}
|
||||
|
||||
export type CustomIconAndTooltipContent = {
|
||||
|
@ -156,6 +158,10 @@ export class AbstractLayer implements ILayer {
|
|||
return mbStyle.sources[sourceId].data;
|
||||
}
|
||||
|
||||
getDescriptor(): LayerDescriptor {
|
||||
return this._descriptor;
|
||||
}
|
||||
|
||||
async cloneDescriptor(): Promise<LayerDescriptor> {
|
||||
const clonedDescriptor = copyPersistentState(this._descriptor);
|
||||
// layer id is uuid used to track styles/layers in mapbox
|
||||
|
@ -259,10 +265,16 @@ export class AbstractLayer implements ILayer {
|
|||
}
|
||||
|
||||
async getAttributions(): Promise<Attribution[]> {
|
||||
if (!this.hasErrors()) {
|
||||
return await this.getSource().getAttributions();
|
||||
if (this.hasErrors() || !this.isVisible()) {
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
|
||||
const attributionProvider = this.getSource().getAttributionProvider();
|
||||
if (attributionProvider) {
|
||||
return attributionProvider();
|
||||
}
|
||||
|
||||
return this._descriptor.attribution !== undefined ? [this._descriptor.attribution] : [];
|
||||
}
|
||||
|
||||
getStyleForEditing(): IStyle {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { Feature } from 'geojson';
|
||||
import { Adapters } from 'src/plugins/inspector/public';
|
||||
import { FileLayer } from '@elastic/ems-client';
|
||||
import { Attribution, ImmutableSourceProperty, SourceEditorArgs } from '../source';
|
||||
import { ImmutableSourceProperty, SourceEditorArgs } from '../source';
|
||||
import { AbstractVectorSource, GeoJsonWithMeta, IVectorSource } from '../vector_source';
|
||||
import { SOURCE_TYPES, FIELD_ORIGIN, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
|
||||
import { getEmsFileLayers } from '../../../util';
|
||||
|
@ -183,9 +183,11 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc
|
|||
}
|
||||
}
|
||||
|
||||
async getAttributions(): Promise<Attribution[]> {
|
||||
const emsFileLayer = await this.getEMSFileLayer();
|
||||
return emsFileLayer.getAttributions();
|
||||
getAttributionProvider() {
|
||||
return async () => {
|
||||
const emsFileLayer = await this.getEMSFileLayer();
|
||||
return emsFileLayer.getAttributions();
|
||||
};
|
||||
}
|
||||
|
||||
async getLeftJoinFields() {
|
||||
|
|
|
@ -113,13 +113,15 @@ export class EMSTMSSource extends AbstractTMSSource {
|
|||
}
|
||||
}
|
||||
|
||||
async getAttributions() {
|
||||
const emsTMSService = await this._getEMSTMSService();
|
||||
const markdown = emsTMSService.getMarkdownAttribution();
|
||||
if (!markdown) {
|
||||
return [];
|
||||
}
|
||||
return this.convertMarkdownLinkToObjectArr(markdown);
|
||||
getAttributionProvider() {
|
||||
return async () => {
|
||||
const emsTMSService = await this._getEMSTMSService();
|
||||
const markdown = emsTMSService.getMarkdownAttribution();
|
||||
if (!markdown) {
|
||||
return [];
|
||||
}
|
||||
return this.convertMarkdownLinkToObjectArr(markdown);
|
||||
};
|
||||
}
|
||||
|
||||
async getUrlTemplate() {
|
||||
|
|
|
@ -42,7 +42,8 @@ describe('EMSTMSSource', () => {
|
|||
id: 'road_map',
|
||||
});
|
||||
|
||||
const attributions = await emsTmsSource.getAttributions();
|
||||
const attributionProvider = emsTmsSource.getAttributionProvider();
|
||||
const attributions = await attributionProvider();
|
||||
expect(attributions).toEqual([
|
||||
{
|
||||
label: 'foobar',
|
||||
|
|
|
@ -53,11 +53,13 @@ export class KibanaTilemapSource extends AbstractTMSSource {
|
|||
return tilemap.url;
|
||||
}
|
||||
|
||||
async getAttributions() {
|
||||
const tilemap = getKibanaTileMap();
|
||||
const markdown = _.get(tilemap, 'options.attribution', '');
|
||||
const objArr = this.convertMarkdownLinkToObjectArr(markdown);
|
||||
return objArr;
|
||||
getAttributionProvider() {
|
||||
return async () => {
|
||||
const tilemap = getKibanaTileMap();
|
||||
const markdown = _.get(tilemap, 'options.attribution', '');
|
||||
const objArr = this.convertMarkdownLinkToObjectArr(markdown);
|
||||
return objArr;
|
||||
};
|
||||
}
|
||||
|
||||
async getDisplayName() {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { copyPersistentState } from '../../reducers/copy_persistent_state';
|
|||
|
||||
import { IField } from '../fields/field';
|
||||
import { FieldFormatter, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
|
||||
import { AbstractSourceDescriptor } from '../../../common/descriptor_types';
|
||||
import { AbstractSourceDescriptor, Attribution } from '../../../common/descriptor_types';
|
||||
import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view';
|
||||
import { LICENSED_FEATURES } from '../../licensed_features';
|
||||
import { PreIndexedShape } from '../../../common/elasticsearch_util';
|
||||
|
@ -31,11 +31,6 @@ export type ImmutableSourceProperty = {
|
|||
link?: string;
|
||||
};
|
||||
|
||||
export type Attribution = {
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export interface ISource {
|
||||
destroy(): void;
|
||||
getDisplayName(): Promise<string>;
|
||||
|
@ -47,7 +42,7 @@ export interface ISource {
|
|||
isRefreshTimerAware(): boolean;
|
||||
isTimeAware(): Promise<boolean>;
|
||||
getImmutableProperties(): Promise<ImmutableSourceProperty[]>;
|
||||
getAttributions(): Promise<Attribution[]>;
|
||||
getAttributionProvider(): (() => Promise<Attribution[]>) | null;
|
||||
isESSource(): boolean;
|
||||
renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement<any> | null;
|
||||
supportsFitToBounds(): Promise<boolean>;
|
||||
|
@ -103,8 +98,8 @@ export class AbstractSource implements ISource {
|
|||
return '';
|
||||
}
|
||||
|
||||
async getAttributions(): Promise<Attribution[]> {
|
||||
return [];
|
||||
getAttributionProvider(): (() => Promise<Attribution[]>) | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
isFieldAware(): boolean {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AbstractSource, Attribution, ISource } from '../source';
|
||||
import { AbstractSource, ISource } from '../source';
|
||||
|
||||
export interface ITMSSource extends ISource {
|
||||
getUrlTemplate(): Promise<string>;
|
||||
|
@ -13,5 +13,4 @@ export interface ITMSSource extends ISource {
|
|||
|
||||
export class AbstractTMSSource extends AbstractSource implements ITMSSource {
|
||||
getUrlTemplate(): Promise<string>;
|
||||
getAttributions(): Promise<Attribution[]>;
|
||||
}
|
||||
|
|
|
@ -39,8 +39,6 @@ export class WMSCreateSourceEditor extends Component {
|
|||
styleOptions: [],
|
||||
selectedLayerOptions: [],
|
||||
selectedStyleOptions: [],
|
||||
attributionText: '',
|
||||
attributionUrl: '',
|
||||
};
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
|
@ -51,7 +49,7 @@ export class WMSCreateSourceEditor extends Component {
|
|||
}
|
||||
|
||||
_previewIfPossible = _.debounce(() => {
|
||||
const { serviceUrl, layers, styles, attributionText, attributionUrl } = this.state;
|
||||
const { serviceUrl, layers, styles } = this.state;
|
||||
|
||||
const sourceConfig =
|
||||
serviceUrl && layers
|
||||
|
@ -59,8 +57,6 @@ export class WMSCreateSourceEditor extends Component {
|
|||
serviceUrl,
|
||||
layers,
|
||||
styles,
|
||||
attributionText,
|
||||
attributionUrl,
|
||||
}
|
||||
: null;
|
||||
this.props.onSourceConfigChange(sourceConfig);
|
||||
|
@ -155,15 +151,6 @@ export class WMSCreateSourceEditor extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
_handleWMSAttributionChange(attributionUpdate) {
|
||||
const { attributionText, attributionUrl } = this.state;
|
||||
this.setState(attributionUpdate, () => {
|
||||
if (attributionText && attributionUrl) {
|
||||
this._previewIfPossible();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_renderLayerAndStyleInputs() {
|
||||
if (!this.state.hasAttemptedToLoadCapabilities || this.state.isLoadingCapabilities) {
|
||||
return null;
|
||||
|
@ -245,49 +232,6 @@ export class WMSCreateSourceEditor extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderAttributionInputs() {
|
||||
if (!this.state.layers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { attributionText, attributionUrl } = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label="Attribution text"
|
||||
isInvalid={attributionUrl !== '' && attributionText === ''}
|
||||
error={[
|
||||
i18n.translate('xpack.maps.source.wms.attributionText', {
|
||||
defaultMessage: 'Attribution url must have accompanying text',
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={({ target }) =>
|
||||
this._handleWMSAttributionChange({ attributionText: target.value })
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label="Attribution link"
|
||||
isInvalid={attributionText !== '' && attributionUrl === ''}
|
||||
error={[
|
||||
i18n.translate('xpack.maps.source.wms.attributionLink', {
|
||||
defaultMessage: 'Attribution text must have an accompanying link',
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={({ target }) =>
|
||||
this._handleWMSAttributionChange({ attributionUrl: target.value })
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPanel>
|
||||
|
@ -302,8 +246,6 @@ export class WMSCreateSourceEditor extends Component {
|
|||
{this._renderGetCapabilitiesButton()}
|
||||
|
||||
{this._renderLayerAndStyleInputs()}
|
||||
|
||||
{this._renderAttributionInputs()}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,14 +19,12 @@ export const sourceTitle = i18n.translate('xpack.maps.source.wmsTitle', {
|
|||
export class WMSSource extends AbstractTMSSource {
|
||||
static type = SOURCE_TYPES.WMS;
|
||||
|
||||
static createDescriptor({ serviceUrl, layers, styles, attributionText, attributionUrl }) {
|
||||
static createDescriptor({ serviceUrl, layers, styles }) {
|
||||
return {
|
||||
type: WMSSource.type,
|
||||
serviceUrl,
|
||||
layers,
|
||||
styles,
|
||||
attributionText,
|
||||
attributionUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -53,20 +51,6 @@ export class WMSSource extends AbstractTMSSource {
|
|||
return this._descriptor.serviceUrl;
|
||||
}
|
||||
|
||||
getAttributions() {
|
||||
const { attributionText, attributionUrl } = this._descriptor;
|
||||
const attributionComplete = !!attributionText && !!attributionUrl;
|
||||
|
||||
return attributionComplete
|
||||
? [
|
||||
{
|
||||
url: attributionUrl,
|
||||
label: attributionText,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
getUrlTemplate() {
|
||||
const client = new WmsClient({ serviceUrl: this._descriptor.serviceUrl });
|
||||
return client.getUrlTemplate(this._descriptor.layers, this._descriptor.styles || '');
|
||||
|
|
|
@ -1,182 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`attribution validation should provide no validation errors when attribution text and attribution url are provided 1`] = `
|
||||
<EuiPanel>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Url"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
placeholder="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={
|
||||
Array [
|
||||
"Attribution url must have accompanying text",
|
||||
]
|
||||
}
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Attribution text"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
value="myAttribtionLabel"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={
|
||||
Array [
|
||||
"Attribution text must have an accompanying link",
|
||||
]
|
||||
}
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Attribution link"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
value="http://mySource"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
`;
|
||||
|
||||
exports[`attribution validation should provide validation error when attribution text is provided without attribution url 1`] = `
|
||||
<EuiPanel>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Url"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
placeholder="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={
|
||||
Array [
|
||||
"Attribution url must have accompanying text",
|
||||
]
|
||||
}
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Attribution text"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
value="myAttribtionLabel"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={
|
||||
Array [
|
||||
"Attribution text must have an accompanying link",
|
||||
]
|
||||
}
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={true}
|
||||
label="Attribution link"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
`;
|
||||
|
||||
exports[`attribution validation should provide validation error when attribution url is provided without attribution text 1`] = `
|
||||
<EuiPanel>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Url"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
placeholder="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={
|
||||
Array [
|
||||
"Attribution url must have accompanying text",
|
||||
]
|
||||
}
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={true}
|
||||
label="Attribution text"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={
|
||||
Array [
|
||||
"Attribution text must have an accompanying link",
|
||||
]
|
||||
}
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Attribution link"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
value="http://mySource"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
`;
|
||||
|
||||
exports[`should render 1`] = `
|
||||
<EuiPanel>
|
||||
<EuiFormRow
|
||||
|
@ -193,45 +16,5 @@ exports[`should render 1`] = `
|
|||
placeholder="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={
|
||||
Array [
|
||||
"Attribution url must have accompanying text",
|
||||
]
|
||||
}
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Attribution text"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={
|
||||
Array [
|
||||
"Attribution text must have an accompanying link",
|
||||
]
|
||||
}
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Attribution link"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
`;
|
||||
|
|
|
@ -15,24 +15,3 @@ test('should render', () => {
|
|||
const component = shallow(<XYZTMSEditor onSourceConfigChange={onSourceConfigChange} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('attribution validation', () => {
|
||||
test('should provide validation error when attribution text is provided without attribution url', () => {
|
||||
const component = shallow(<XYZTMSEditor onSourceConfigChange={onSourceConfigChange} />);
|
||||
component.setState({ attributionText: 'myAttribtionLabel' });
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should provide validation error when attribution url is provided without attribution text', () => {
|
||||
const component = shallow(<XYZTMSEditor onSourceConfigChange={onSourceConfigChange} />);
|
||||
component.setState({ attributionUrl: 'http://mySource' });
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should provide no validation errors when attribution text and attribution url are provided', () => {
|
||||
const component = shallow(<XYZTMSEditor onSourceConfigChange={onSourceConfigChange} />);
|
||||
component.setState({ attributionText: 'myAttribtionLabel' });
|
||||
component.setState({ attributionUrl: 'http://mySource' });
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,12 +10,9 @@
|
|||
import React, { Component, ChangeEvent } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { EuiFormRow, EuiFieldText, EuiPanel } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export type XYZTMSSourceConfig = {
|
||||
urlTemplate: string;
|
||||
attributionText: string;
|
||||
attributionUrl: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
|
@ -24,29 +21,19 @@ interface Props {
|
|||
|
||||
interface State {
|
||||
url: string;
|
||||
attributionText: string;
|
||||
attributionUrl: string;
|
||||
}
|
||||
|
||||
export class XYZTMSEditor extends Component<Props, State> {
|
||||
state = {
|
||||
url: '',
|
||||
attributionText: '',
|
||||
attributionUrl: '',
|
||||
};
|
||||
|
||||
_previewLayer = _.debounce(() => {
|
||||
const { url, attributionText, attributionUrl } = this.state;
|
||||
const { url } = this.state;
|
||||
|
||||
const isUrlValid =
|
||||
url.indexOf('{x}') >= 0 && url.indexOf('{y}') >= 0 && url.indexOf('{z}') >= 0;
|
||||
const sourceConfig = isUrlValid
|
||||
? {
|
||||
urlTemplate: url,
|
||||
attributionText,
|
||||
attributionUrl,
|
||||
}
|
||||
: null;
|
||||
const sourceConfig = isUrlValid ? { urlTemplate: url } : null;
|
||||
this.props.onSourceConfigChange(sourceConfig);
|
||||
}, 500);
|
||||
|
||||
|
@ -54,16 +41,7 @@ export class XYZTMSEditor extends Component<Props, State> {
|
|||
this.setState({ url: event.target.value }, this._previewLayer);
|
||||
};
|
||||
|
||||
_onAttributionTextChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ attributionText: event.target.value }, this._previewLayer);
|
||||
};
|
||||
|
||||
_onAttributionUrlChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ attributionUrl: event.target.value }, this._previewLayer);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { attributionText, attributionUrl } = this.state;
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiFormRow label="Url">
|
||||
|
@ -72,32 +50,6 @@ export class XYZTMSEditor extends Component<Props, State> {
|
|||
onChange={this._onUrlChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.xyztmssource.attributionTextLabel', {
|
||||
defaultMessage: 'Attribution text',
|
||||
})}
|
||||
isInvalid={attributionUrl !== '' && attributionText === ''}
|
||||
error={[
|
||||
i18n.translate('xpack.maps.xyztmssource.attributionText', {
|
||||
defaultMessage: 'Attribution url must have accompanying text',
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<EuiFieldText value={attributionText} onChange={this._onAttributionTextChange} />
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.xyztmssource.attributionLinkLabel', {
|
||||
defaultMessage: 'Attribution link',
|
||||
})}
|
||||
isInvalid={attributionText !== '' && attributionUrl === ''}
|
||||
error={[
|
||||
i18n.translate('xpack.maps.xyztmssource.attributionLink', {
|
||||
defaultMessage: 'Attribution text must have an accompanying link',
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<EuiFieldText value={attributionUrl} onChange={this._onAttributionUrlChange} />
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { SOURCE_TYPES } from '../../../../common/constants';
|
|||
import { registerSource } from '../source_registry';
|
||||
import { AbstractTMSSource } from '../tms_source';
|
||||
import { XYZTMSSourceDescriptor } from '../../../../common/descriptor_types';
|
||||
import { Attribution, ImmutableSourceProperty } from '../source';
|
||||
import { ImmutableSourceProperty } from '../source';
|
||||
import { XYZTMSSourceConfig } from './xyz_tms_editor';
|
||||
|
||||
export const sourceTitle = i18n.translate('xpack.maps.source.ems_xyzTitle', {
|
||||
|
@ -23,16 +23,10 @@ export class XYZTMSSource extends AbstractTMSSource {
|
|||
|
||||
readonly _descriptor: XYZTMSSourceDescriptor;
|
||||
|
||||
static createDescriptor({
|
||||
urlTemplate,
|
||||
attributionText,
|
||||
attributionUrl,
|
||||
}: XYZTMSSourceConfig): XYZTMSSourceDescriptor {
|
||||
static createDescriptor({ urlTemplate }: XYZTMSSourceConfig): XYZTMSSourceDescriptor {
|
||||
return {
|
||||
type: XYZTMSSource.type,
|
||||
urlTemplate,
|
||||
attributionText,
|
||||
attributionUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -52,19 +46,6 @@ export class XYZTMSSource extends AbstractTMSSource {
|
|||
return this._descriptor.urlTemplate;
|
||||
}
|
||||
|
||||
async getAttributions(): Promise<Attribution[]> {
|
||||
const { attributionText, attributionUrl } = this._descriptor;
|
||||
const attributionComplete = !!attributionText && !!attributionUrl;
|
||||
return attributionComplete
|
||||
? [
|
||||
{
|
||||
url: attributionUrl as string,
|
||||
label: attributionText as string,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
async getUrlTemplate(): Promise<string> {
|
||||
return this._descriptor.urlTemplate;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@import 'layer_panel';
|
||||
@import 'layer_settings/index';
|
||||
@import 'filter_editor/filter_editor';
|
||||
@import 'join_editor/resources/join';
|
||||
@import 'style_settings/style_settings';
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Should render add form row when attribution not provided 1`] = `
|
||||
<fieldset
|
||||
aria-labelledby="mapsLayerSettingsAttributionLegend"
|
||||
>
|
||||
<div
|
||||
className="mapAttributionFormRow"
|
||||
>
|
||||
<legend
|
||||
className="mapAttributionFormRow__legend"
|
||||
id="mapsLayerSettingsAttributionLegend"
|
||||
>
|
||||
Attribution
|
||||
</legend>
|
||||
<div
|
||||
className="mapAttributionFormRow__field"
|
||||
>
|
||||
<AttributionPopover
|
||||
label=""
|
||||
onChange={[Function]}
|
||||
popoverButtonAriaLabel="Add attribution"
|
||||
popoverButtonClassName="mapAttributionFormRow__addButton"
|
||||
popoverButtonIcon="plusInCircleFilled"
|
||||
popoverButtonLabel="Add attribution"
|
||||
url=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
exports[`Should render edit form row when attribution not provided 1`] = `
|
||||
<fieldset
|
||||
aria-labelledby="mapsLayerSettingsAttributionLegend"
|
||||
>
|
||||
<div
|
||||
className="mapAttributionFormRow"
|
||||
>
|
||||
<legend
|
||||
className="mapAttributionFormRow__legend"
|
||||
id="mapsLayerSettingsAttributionLegend"
|
||||
>
|
||||
Attribution
|
||||
</legend>
|
||||
<div
|
||||
className="mapAttributionFormRow__field"
|
||||
>
|
||||
<EuiPanel
|
||||
color="subdued"
|
||||
paddingSize="s"
|
||||
>
|
||||
<EuiLink
|
||||
color="text"
|
||||
href="url1"
|
||||
target="_blank"
|
||||
>
|
||||
label1
|
||||
</EuiLink>
|
||||
</EuiPanel>
|
||||
<div
|
||||
className="mapAttributionFormRow__buttons"
|
||||
>
|
||||
<AttributionPopover
|
||||
label="label1"
|
||||
onChange={[Function]}
|
||||
popoverButtonAriaLabel="Edit attribution"
|
||||
popoverButtonIcon="pencil"
|
||||
popoverButtonLabel="Edit"
|
||||
url="url1"
|
||||
/>
|
||||
<EuiButtonEmpty
|
||||
aria-label="Clear attribution"
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={[Function]}
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Clear"
|
||||
id="xpack.maps.attribution.clearBtnLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
exports[`Should render null when layer source has attribution provider 1`] = `""`;
|
|
@ -0,0 +1,27 @@
|
|||
.mapAttributionFormRow {
|
||||
display: flex;
|
||||
margin: $euiSizeS 0;
|
||||
}
|
||||
|
||||
.mapAttributionFormRow__legend {
|
||||
@include euiFormLabel;
|
||||
display: inline-flex;
|
||||
width: calc(33% - #{$euiSizeS});
|
||||
height: $euiSizeXL;
|
||||
align-items: center;
|
||||
margin-right: $euiSizeS;
|
||||
}
|
||||
|
||||
.mapAttributionFormRow__field {
|
||||
width: 67%;
|
||||
}
|
||||
|
||||
.mapAttributionFormRow__addButton {
|
||||
margin-top: $euiSizeXS;
|
||||
}
|
||||
|
||||
.mapAttributionFormRow__buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: $euiSizeXS;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.mapAttributionPopover {
|
||||
width: $euiSizeXXL * 12;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
@import 'attribution_form_row';
|
||||
@import 'attribution_popover';
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import { LayerDescriptor } from '../../../../common/descriptor_types';
|
||||
import { ILayer } from '../../../classes/layers/layer';
|
||||
import { ISource } from '../../../classes/sources/source';
|
||||
import { AttributionFormRow } from './attribution_form_row';
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
test('Should render null when layer source has attribution provider', () => {
|
||||
const sourceMock = ({
|
||||
getAttributionProvider: () => {
|
||||
return async () => {
|
||||
return [{ url: 'url1', label: 'label1' }];
|
||||
};
|
||||
},
|
||||
} as unknown) as ISource;
|
||||
const layerMock = ({
|
||||
getSource: () => {
|
||||
return sourceMock;
|
||||
},
|
||||
} as unknown) as ILayer;
|
||||
const component = shallow(<AttributionFormRow {...defaultProps} layer={layerMock} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should render add form row when attribution not provided', () => {
|
||||
const sourceMock = ({
|
||||
getAttributionProvider: () => {
|
||||
return null;
|
||||
},
|
||||
} as unknown) as ISource;
|
||||
const layerMock = ({
|
||||
getSource: () => {
|
||||
return sourceMock;
|
||||
},
|
||||
getDescriptor: () => {
|
||||
return ({} as unknown) as LayerDescriptor;
|
||||
},
|
||||
} as unknown) as ILayer;
|
||||
const component = shallow(<AttributionFormRow {...defaultProps} layer={layerMock} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should render edit form row when attribution not provided', () => {
|
||||
const sourceMock = ({
|
||||
getAttributionProvider: () => {
|
||||
return null;
|
||||
},
|
||||
} as unknown) as ISource;
|
||||
const layerMock = ({
|
||||
getSource: () => {
|
||||
return sourceMock;
|
||||
},
|
||||
getDescriptor: () => {
|
||||
return ({
|
||||
attribution: {
|
||||
url: 'url1',
|
||||
label: 'label1',
|
||||
},
|
||||
} as unknown) as LayerDescriptor;
|
||||
},
|
||||
} as unknown) as ILayer;
|
||||
const component = shallow(<AttributionFormRow {...defaultProps} layer={layerMock} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiLink, EuiPanel } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Attribution } from '../../../../common/descriptor_types';
|
||||
import { ILayer } from '../../../classes/layers/layer';
|
||||
import { AttributionPopover } from './attribution_popover';
|
||||
|
||||
interface Props {
|
||||
layer: ILayer;
|
||||
onChange: (attribution?: Attribution) => void;
|
||||
}
|
||||
|
||||
export function AttributionFormRow(props: Props) {
|
||||
function renderAttribution() {
|
||||
const layerDescriptor = props.layer.getDescriptor();
|
||||
|
||||
return (
|
||||
<fieldset aria-labelledby="mapsLayerSettingsAttributionLegend">
|
||||
<div className="mapAttributionFormRow">
|
||||
<legend id="mapsLayerSettingsAttributionLegend" className="mapAttributionFormRow__legend">
|
||||
{i18n.translate('xpack.maps.layerSettings.attributionLegend', {
|
||||
defaultMessage: 'Attribution',
|
||||
})}
|
||||
</legend>
|
||||
|
||||
{layerDescriptor.attribution === undefined ? (
|
||||
<div className="mapAttributionFormRow__field">
|
||||
<AttributionPopover
|
||||
onChange={props.onChange}
|
||||
popoverButtonIcon="plusInCircleFilled"
|
||||
popoverButtonLabel={i18n.translate('xpack.maps.attribution.addBtnLabel', {
|
||||
defaultMessage: 'Add attribution',
|
||||
})}
|
||||
popoverButtonAriaLabel={i18n.translate('xpack.maps.attribution.addBtnAriaLabel', {
|
||||
defaultMessage: 'Add attribution',
|
||||
})}
|
||||
popoverButtonClassName="mapAttributionFormRow__addButton"
|
||||
label={''}
|
||||
url={''}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mapAttributionFormRow__field">
|
||||
<EuiPanel color="subdued" paddingSize="s">
|
||||
<EuiLink color="text" href={layerDescriptor.attribution.url} target="_blank">
|
||||
{layerDescriptor.attribution.label}
|
||||
</EuiLink>
|
||||
</EuiPanel>
|
||||
|
||||
<div className="mapAttributionFormRow__buttons">
|
||||
<AttributionPopover
|
||||
onChange={props.onChange}
|
||||
popoverButtonIcon="pencil"
|
||||
popoverButtonAriaLabel={i18n.translate(
|
||||
'xpack.maps.attribution.editBtnAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Edit attribution',
|
||||
}
|
||||
)}
|
||||
popoverButtonLabel={i18n.translate('xpack.maps.attribution.editBtnLabel', {
|
||||
defaultMessage: 'Edit',
|
||||
})}
|
||||
label={layerDescriptor.attribution.label}
|
||||
url={layerDescriptor.attribution.url}
|
||||
/>
|
||||
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
props.onChange();
|
||||
}}
|
||||
size="xs"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
aria-label={i18n.translate('xpack.maps.attribution.clearBtnAriaLabel', {
|
||||
defaultMessage: 'Clear attribution',
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.attribution.clearBtnLabel"
|
||||
defaultMessage="Clear"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
return props.layer.getSource().getAttributionProvider() ? null : renderAttribution();
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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, { ChangeEvent, Component } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiPopover,
|
||||
EuiPopoverFooter,
|
||||
EuiPopoverTitle,
|
||||
EuiSpacer,
|
||||
EuiTextAlign,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Attribution } from '../../../../common/descriptor_types';
|
||||
|
||||
interface Props {
|
||||
onChange: (attribution: Attribution) => void;
|
||||
popoverButtonLabel: string;
|
||||
popoverButtonAriaLabel: string;
|
||||
popoverButtonIcon: string;
|
||||
popoverButtonClassName?: string;
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export class AttributionPopover extends Component<Props, State> {
|
||||
state: State = {
|
||||
isPopoverOpen: false,
|
||||
label: this.props.label,
|
||||
url: this.props.url,
|
||||
};
|
||||
|
||||
_togglePopover = () => {
|
||||
this.setState((prevState) => ({
|
||||
isPopoverOpen: !prevState.isPopoverOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
_closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
_onApply = () => {
|
||||
this.props.onChange({
|
||||
label: this.state.label,
|
||||
url: this.state.url,
|
||||
});
|
||||
this._closePopover();
|
||||
};
|
||||
|
||||
_onLabelChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ label: event.target.value });
|
||||
};
|
||||
|
||||
_onUrlChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ url: event.target.value });
|
||||
};
|
||||
|
||||
_renderPopoverButton() {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
className={this.props.popoverButtonClassName}
|
||||
aria-label={this.props.popoverButtonAriaLabel}
|
||||
onClick={this._togglePopover}
|
||||
size="xs"
|
||||
iconType={this.props.popoverButtonIcon}
|
||||
flush="left"
|
||||
>
|
||||
{this.props.popoverButtonLabel}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
_renderContent() {
|
||||
const isComplete = this.state.label.length !== 0 && this.state.url.length !== 0;
|
||||
const hasChanges = this.state.label !== this.props.label || this.state.url !== this.props.url;
|
||||
return (
|
||||
<div className="mapAttributionPopover">
|
||||
<EuiPopoverTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.attribution.attributionFormLabel"
|
||||
defaultMessage="Attribution"
|
||||
/>
|
||||
</EuiPopoverTitle>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.attribution.labelFieldLabel', {
|
||||
defaultMessage: 'Label',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed
|
||||
fullWidth
|
||||
value={this.state.label}
|
||||
onChange={this._onLabelChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.attribution.urlLabel', {
|
||||
defaultMessage: 'Link',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText compressed fullWidth value={this.state.url} onChange={this._onUrlChange} />
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiPopoverFooter>
|
||||
<EuiTextAlign textAlign="right">
|
||||
<EuiButton
|
||||
fill
|
||||
isDisabled={!isComplete || !hasChanges}
|
||||
onClick={this._onApply}
|
||||
size="s"
|
||||
>
|
||||
<FormattedMessage id="xpack.maps.attribution.applyBtnLabel" defaultMessage="Apply" />
|
||||
</EuiButton>
|
||||
</EuiTextAlign>
|
||||
</EuiPopoverFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiPopover
|
||||
id="attributionPopover"
|
||||
panelPaddingSize="s"
|
||||
anchorPosition="leftCenter"
|
||||
button={this._renderPopoverButton()}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this._closePopover}
|
||||
ownFocus
|
||||
>
|
||||
{this._renderContent()}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -9,15 +9,21 @@ import { AnyAction, Dispatch } from 'redux';
|
|||
import { connect } from 'react-redux';
|
||||
import { LayerSettings } from './layer_settings';
|
||||
import {
|
||||
clearLayerAttribution,
|
||||
setLayerAttribution,
|
||||
updateLayerLabel,
|
||||
updateLayerMaxZoom,
|
||||
updateLayerMinZoom,
|
||||
updateLayerAlpha,
|
||||
updateLabelsOnTop,
|
||||
} from '../../../actions';
|
||||
import { Attribution } from '../../../../common/descriptor_types';
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
|
||||
return {
|
||||
clearLayerAttribution: (id: string) => dispatch(clearLayerAttribution(id)),
|
||||
setLayerAttribution: (id: string, attribution: Attribution) =>
|
||||
dispatch(setLayerAttribution(id, attribution)),
|
||||
updateLabel: (id: string, label: string) => dispatch(updateLayerLabel(id, label)),
|
||||
updateMinZoom: (id: string, minZoom: number) => dispatch(updateLayerMinZoom(id, minZoom)),
|
||||
updateMaxZoom: (id: string, maxZoom: number) => dispatch(updateLayerMaxZoom(id, maxZoom)),
|
||||
|
|
|
@ -17,13 +17,17 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Attribution } from '../../../../common/descriptor_types';
|
||||
import { MAX_ZOOM } from '../../../../common/constants';
|
||||
import { AlphaSlider } from '../../../components/alpha_slider';
|
||||
import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ILayer } from '../../../classes/layers/layer';
|
||||
import { AttributionFormRow } from './attribution_form_row';
|
||||
|
||||
export interface Props {
|
||||
layer: ILayer;
|
||||
clearLayerAttribution: (layerId: string) => void;
|
||||
setLayerAttribution: (id: string, attribution: Attribution) => void;
|
||||
updateLabel: (layerId: string, label: string) => void;
|
||||
updateMinZoom: (layerId: string, minZoom: number) => void;
|
||||
updateMaxZoom: (layerId: string, maxZoom: number) => void;
|
||||
|
@ -54,6 +58,14 @@ export function LayerSettings(props: Props) {
|
|||
props.updateLabelsOnTop(layerId, event.target.checked);
|
||||
};
|
||||
|
||||
const onAttributionChange = (attribution?: Attribution) => {
|
||||
if (attribution) {
|
||||
props.setLayerAttribution(layerId, attribution);
|
||||
} else {
|
||||
props.clearLayerAttribution(layerId);
|
||||
}
|
||||
};
|
||||
|
||||
const renderZoomSliders = () => {
|
||||
return (
|
||||
<ValidatedDualRange
|
||||
|
@ -127,6 +139,7 @@ export function LayerSettings(props: Props) {
|
|||
{renderZoomSliders()}
|
||||
<AlphaSlider alpha={props.layer.getAlpha()} onChange={onAlphaChange} />
|
||||
{renderShowLabelsOnTop()}
|
||||
<AttributionFormRow layer={props.layer} onChange={onAttributionChange} />
|
||||
</EuiPanel>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { Component, Fragment } from 'react';
|
|||
import _ from 'lodash';
|
||||
import { EuiText, EuiLink } from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import { Attribution } from '../../../classes/sources/source';
|
||||
import { Attribution } from '../../../../common/descriptor_types';
|
||||
import { ILayer } from '../../../classes/layers/layer';
|
||||
|
||||
export interface Props {
|
||||
|
@ -65,6 +65,7 @@ export class AttributionControl extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reflect top-to-bottom layer order as left-to-right in attribs
|
||||
uniqueAttributions.reverse();
|
||||
if (!_.isEqual(this.state.uniqueAttributions, uniqueAttributions)) {
|
||||
|
|
|
@ -17,6 +17,33 @@ export function findLayerById(state: MapState, layerId: string): LayerDescriptor
|
|||
return state.layerList.find(({ id }) => layerId === id);
|
||||
}
|
||||
|
||||
export function clearLayerProp(
|
||||
state: MapState,
|
||||
layerId: string,
|
||||
propName: keyof LayerDescriptor
|
||||
): MapState {
|
||||
if (!layerId) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const { layerList } = state;
|
||||
const layerIdx = getLayerIndex(layerList, layerId);
|
||||
if (layerIdx === -1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const updatedLayer = {
|
||||
...layerList[layerIdx],
|
||||
};
|
||||
delete updatedLayer[propName];
|
||||
const updatedList = [
|
||||
...layerList.slice(0, layerIdx),
|
||||
updatedLayer,
|
||||
...layerList.slice(layerIdx + 1),
|
||||
];
|
||||
return { ...state, layerList: updatedList };
|
||||
}
|
||||
|
||||
export function updateLayerInList(
|
||||
state: MapState,
|
||||
layerId: string,
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
ADD_LAYER,
|
||||
SET_LAYER_ERROR_STATUS,
|
||||
ADD_WAITING_FOR_MAP_READY_LAYER,
|
||||
CLEAR_LAYER_PROP,
|
||||
CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST,
|
||||
REMOVE_LAYER,
|
||||
SET_LAYER_VISIBILITY,
|
||||
|
@ -49,6 +50,7 @@ import {
|
|||
|
||||
import { getDefaultMapSettings } from './default_map_settings';
|
||||
import {
|
||||
clearLayerProp,
|
||||
getLayerIndex,
|
||||
removeTrackedLayerState,
|
||||
rollbackTrackedLayerState,
|
||||
|
@ -258,6 +260,8 @@ export function map(state: MapState = DEFAULT_MAP_STATE, action: any) {
|
|||
};
|
||||
case UPDATE_LAYER_PROP:
|
||||
return updateLayerInList(state, action.id, action.propName, action.newValue);
|
||||
case CLEAR_LAYER_PROP:
|
||||
return clearLayerProp(state, action.id, action.propName);
|
||||
case UPDATE_SOURCE_PROP:
|
||||
return updateLayerSourceDescriptorProp(state, action.layerId, action.propName, action.value);
|
||||
case SET_JOINS:
|
||||
|
|
|
@ -16,6 +16,7 @@ import { migrateJoinAggKey } from '../../common/migrations/join_agg_key';
|
|||
import { removeBoundsFromSavedObject } from '../../common/migrations/remove_bounds';
|
||||
import { setDefaultAutoFitToBounds } from '../../common/migrations/set_default_auto_fit_to_bounds';
|
||||
import { addTypeToTermJoin } from '../../common/migrations/add_type_to_termjoin';
|
||||
import { moveAttribution } from '../../common/migrations/move_attribution';
|
||||
|
||||
export const migrations = {
|
||||
map: {
|
||||
|
@ -89,6 +90,14 @@ export const migrations = {
|
|||
'7.12.0': (doc) => {
|
||||
const attributes = addTypeToTermJoin(doc);
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes,
|
||||
};
|
||||
},
|
||||
'7.14.0': (doc) => {
|
||||
const attributes = moveAttribution(doc);
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes,
|
||||
|
|
|
@ -12711,8 +12711,6 @@
|
|||
"xpack.maps.source.pewPewDescription": "ソースとデスティネーションの間の集約データパスです。",
|
||||
"xpack.maps.source.pewPewTitle": "ソースとデスティネーションの接続",
|
||||
"xpack.maps.source.urlLabel": "Url",
|
||||
"xpack.maps.source.wms.attributionLink": "属性テキストにはリンクが必要です",
|
||||
"xpack.maps.source.wms.attributionText": "属性 URL にはテキストが必要です",
|
||||
"xpack.maps.source.wms.getCapabilitiesButtonText": "負荷容量",
|
||||
"xpack.maps.source.wms.getCapabilitiesErrorCalloutTitle": "サービスメタデータを読み込めません",
|
||||
"xpack.maps.source.wms.layersHelpText": "レイヤー名のコンマ区切りのリストを使用します",
|
||||
|
@ -12858,10 +12856,6 @@
|
|||
"xpack.maps.viewControl.zoomLabel": "ズーム:",
|
||||
"xpack.maps.visTypeAlias.description": "マップを作成し、複数のレイヤーとインデックスを使用して、スタイルを設定します。",
|
||||
"xpack.maps.visTypeAlias.title": "マップ",
|
||||
"xpack.maps.xyztmssource.attributionLink": "属性テキストにはリンクが必要です",
|
||||
"xpack.maps.xyztmssource.attributionLinkLabel": "属性リンク",
|
||||
"xpack.maps.xyztmssource.attributionText": "属性 URL にはテキストが必要です",
|
||||
"xpack.maps.xyztmssource.attributionTextLabel": "属性テキスト",
|
||||
"xpack.ml.accessDenied.description": "ML プラグインへのアクセスパーミッションがありません",
|
||||
"xpack.ml.accessDenied.label": "パーミッションがありません",
|
||||
"xpack.ml.accessDeniedLabel": "アクセスが拒否されました",
|
||||
|
|
|
@ -12880,8 +12880,6 @@
|
|||
"xpack.maps.source.pewPewDescription": "源和目标之间的聚合数据路径",
|
||||
"xpack.maps.source.pewPewTitle": "源-目标连接",
|
||||
"xpack.maps.source.urlLabel": "URL",
|
||||
"xpack.maps.source.wms.attributionLink": "属性文本必须附带链接",
|
||||
"xpack.maps.source.wms.attributionText": "属性 url 必须附带文本",
|
||||
"xpack.maps.source.wms.getCapabilitiesButtonText": "加载功能",
|
||||
"xpack.maps.source.wms.getCapabilitiesErrorCalloutTitle": "无法加载服务元数据",
|
||||
"xpack.maps.source.wms.layersHelpText": "使用图层名称逗号分隔列表",
|
||||
|
@ -13027,10 +13025,6 @@
|
|||
"xpack.maps.viewControl.zoomLabel": "缩放:",
|
||||
"xpack.maps.visTypeAlias.description": "使用多个图层和索引创建地图并提供样式。",
|
||||
"xpack.maps.visTypeAlias.title": "Maps",
|
||||
"xpack.maps.xyztmssource.attributionLink": "属性文本必须附带链接",
|
||||
"xpack.maps.xyztmssource.attributionLinkLabel": "属性链接",
|
||||
"xpack.maps.xyztmssource.attributionText": "属性 url 必须附带文本",
|
||||
"xpack.maps.xyztmssource.attributionTextLabel": "属性文本",
|
||||
"xpack.ml.accessDenied.description": "您无权访问 ML 插件",
|
||||
"xpack.ml.accessDenied.label": "权限不足",
|
||||
"xpack.ml.accessDeniedLabel": "访问被拒绝",
|
||||
|
|
|
@ -42,7 +42,7 @@ export default function ({ getService }) {
|
|||
type: 'index-pattern',
|
||||
},
|
||||
]);
|
||||
expect(resp.body.migrationVersion).to.eql({ map: '7.12.0' });
|
||||
expect(resp.body.migrationVersion).to.eql({ map: '7.14.0' });
|
||||
expect(resp.body.attributes.layerListJSON.includes('indexPatternRefName')).to.be(true);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue