mirror of
https://github.com/apache/superset.git
synced 2026-06-01 13:49:21 +00:00
feat(plugin): add plugin-chart-cartodiagram (#25869)
Co-authored-by: Jakob Miksch <jakob@meggsimum.de>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { css, styled, t } from '@superset-ui/core';
|
||||
import { Button, Tree } from 'antd';
|
||||
import { TreeProps } from 'antd/lib/tree';
|
||||
import { forwardRef } from 'react';
|
||||
import { FlatLayerDataNode, FlatLayerTreeProps, LayerConf } from './types';
|
||||
import { handleDrop } from './dragDropUtil';
|
||||
import LayerTreeItem from './LayerTreeItem';
|
||||
|
||||
export const StyledLayerTreeItem = styled(LayerTreeItem)`
|
||||
${({ theme }) => css`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: unset;
|
||||
|
||||
border: none;
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
background-color: ${theme.colors.grayscale.light3};
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
font-weight: ${theme.typography.weights.normal};
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.colors.grayscale.light3};
|
||||
}
|
||||
|
||||
& .layer-tree-item-close {
|
||||
border-right: solid;
|
||||
border-right-width: 1px;
|
||||
border-right-color: ${theme.colors.grayscale.light2};
|
||||
}
|
||||
|
||||
& .layer-tree-item-edit {
|
||||
border-left: solid;
|
||||
border-left-width: 1px;
|
||||
border-left-color: ${theme.colors.grayscale.light2};
|
||||
}
|
||||
|
||||
& .layer-tree-item-title {
|
||||
flex: 1;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
& .layer-tree-item-type {
|
||||
padding-left: 4px;
|
||||
font-size: ${theme.typography.sizes.xs}px;
|
||||
font-family: ${theme.typography.families.monospace};
|
||||
}
|
||||
|
||||
& > button {
|
||||
border: none;
|
||||
background-color: unset;
|
||||
color: ${theme.colors.grayscale.light1};
|
||||
}
|
||||
|
||||
& > button:hover {
|
||||
background-color: unset;
|
||||
color: ${theme.colors.grayscale.light1};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
// forwardRef is needed here in order for emotion and antd tree to work properly
|
||||
export const FlatLayerTree = forwardRef<HTMLDivElement, FlatLayerTreeProps>(
|
||||
(
|
||||
{
|
||||
layerConfigs,
|
||||
onAddLayer = () => {},
|
||||
onRemoveLayer = () => {},
|
||||
onEditLayer = () => {},
|
||||
onMoveLayer = () => {},
|
||||
draggable,
|
||||
className,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const layerConfigsToTreeData = (
|
||||
configs: LayerConf[],
|
||||
): FlatLayerDataNode[] =>
|
||||
configs.map((config, idx) => ({
|
||||
layerConf: config,
|
||||
key: idx,
|
||||
title: (
|
||||
<StyledLayerTreeItem
|
||||
layerConf={config}
|
||||
onEditClick={() => onEditLayer(config, idx)}
|
||||
onRemoveClick={() => onRemoveLayer(idx)}
|
||||
/>
|
||||
),
|
||||
selectable: false,
|
||||
isLeaf: true,
|
||||
checkable: false,
|
||||
}));
|
||||
|
||||
const treeDataToLayerConfigs = (
|
||||
treeData: FlatLayerDataNode[],
|
||||
): LayerConf[] => treeData.map(data => data.layerConf);
|
||||
|
||||
const treeData = layerConfigsToTreeData(layerConfigs);
|
||||
|
||||
const onDrop: TreeProps['onDrop'] = info => {
|
||||
const data = handleDrop(info, treeData);
|
||||
const movedLayerConfigs = treeDataToLayerConfigs(data);
|
||||
onMoveLayer(movedLayerConfigs);
|
||||
};
|
||||
|
||||
const addLayerLabel = t('Click to add new layer');
|
||||
|
||||
return (
|
||||
<div className={className} ref={ref}>
|
||||
<Button
|
||||
className="add-layer-btn"
|
||||
onClick={onAddLayer}
|
||||
size="small"
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
{addLayerLabel}
|
||||
</Button>
|
||||
<Tree treeData={treeData} draggable={draggable} onDrop={onDrop} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default FlatLayerTree;
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This component is needed to be able to style GeoStyler
|
||||
* via emotion. Emotion can only be used on a component that
|
||||
* accepts a className property.
|
||||
*/
|
||||
import CardStyle from 'geostyler/dist/Component/CardStyle/CardStyle';
|
||||
import { FC } from 'react';
|
||||
import { GeoStylerWrapperProps } from './types';
|
||||
|
||||
export const GeoStylerWrapper: FC<GeoStylerWrapperProps> = ({
|
||||
className,
|
||||
...passThroughProps
|
||||
}) => (
|
||||
<div className={className}>
|
||||
<CardStyle {...passThroughProps} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default GeoStylerWrapper;
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ControlHeader } from '@superset-ui/chart-controls';
|
||||
import { css, styled, t } from '@superset-ui/core';
|
||||
import { Popover } from 'antd';
|
||||
import { FC, useState } from 'react';
|
||||
import { EditItem, LayerConf, LayerConfigsControlProps } from './types';
|
||||
import LayerConfigsPopoverContent from './LayerConfigsPopoverContent';
|
||||
import FlatLayerTree from './FlatLayerTree';
|
||||
|
||||
export const StyledFlatLayerTree = styled(FlatLayerTree)`
|
||||
${({ theme }) => css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border: solid;
|
||||
border-width: 1px;
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
border-color: ${theme.colors.grayscale.light2};
|
||||
|
||||
& .add-layer-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
margin: 4px;
|
||||
|
||||
color: ${theme.colors.grayscale.light1};
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
font-weight: ${theme.typography.weights.normal};
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.colors.grayscale.light4};
|
||||
border-color: ${theme.colors.grayscale.light2};
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-tree .ant-tree-treenode {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .ant-tree-list-holder-inner {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
& .ant-tree-node-content-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .ant-tree-node-content-wrapper:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const getEmptyEditItem = (): EditItem => ({
|
||||
idx: NaN,
|
||||
layerConf: {
|
||||
type: 'WMS',
|
||||
version: '1.3.0',
|
||||
title: '',
|
||||
url: '',
|
||||
layersParam: '',
|
||||
},
|
||||
});
|
||||
|
||||
export const LayerConfigsControl: FC<LayerConfigsControlProps> = ({
|
||||
value,
|
||||
onChange = () => {},
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
renderTrigger,
|
||||
hovered,
|
||||
validationErrors,
|
||||
}) => {
|
||||
const [popoverVisible, setPopoverVisible] = useState<boolean>(false);
|
||||
const [editItem, setEditItem] = useState<EditItem>(getEmptyEditItem());
|
||||
|
||||
const onAddClick = () => {
|
||||
setEditItem(getEmptyEditItem());
|
||||
setPopoverVisible(true);
|
||||
};
|
||||
|
||||
const onEditClick = (layerConf: LayerConf, idx: number) => {
|
||||
if (popoverVisible) {
|
||||
return;
|
||||
}
|
||||
setEditItem({
|
||||
idx,
|
||||
layerConf: { ...layerConf },
|
||||
});
|
||||
setPopoverVisible(true);
|
||||
};
|
||||
|
||||
const onRemoveClick = (idx: number) => {
|
||||
const newValue = value ? [...value] : [];
|
||||
newValue.splice(idx, 1);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const onPopoverClose = () => {
|
||||
setPopoverVisible(false);
|
||||
};
|
||||
|
||||
const computeNewValue = (layerConf: LayerConf) => {
|
||||
const newValue = value ? [...value] : [];
|
||||
if (!editItem) {
|
||||
return undefined;
|
||||
}
|
||||
if (Number.isNaN(editItem.idx)) {
|
||||
newValue.unshift(layerConf);
|
||||
} else if (editItem) {
|
||||
newValue[editItem.idx] = layerConf;
|
||||
}
|
||||
return newValue;
|
||||
};
|
||||
|
||||
const onPopoverSave = (layerConf: LayerConf) => {
|
||||
const newValue = computeNewValue(layerConf);
|
||||
setPopoverVisible(false);
|
||||
if (!newValue) {
|
||||
return;
|
||||
}
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const onMoveLayer = (newConfigs: LayerConf[]) => {
|
||||
onChange(newConfigs);
|
||||
};
|
||||
|
||||
const popoverTitle = editItem.layerConf.title
|
||||
? editItem.layerConf.title
|
||||
: t('Add Layer');
|
||||
const controlHeaderProps = {
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
renderTrigger,
|
||||
hovered,
|
||||
validationErrors,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...controlHeaderProps} />
|
||||
<Popover
|
||||
visible={popoverVisible}
|
||||
trigger="click"
|
||||
title={popoverTitle}
|
||||
placement="right"
|
||||
overlayStyle={{
|
||||
maxWidth: '400px',
|
||||
maxHeight: '700px',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
content={
|
||||
<LayerConfigsPopoverContent
|
||||
layerConf={editItem.layerConf}
|
||||
onClose={onPopoverClose}
|
||||
onSave={onPopoverSave}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StyledFlatLayerTree
|
||||
layerConfigs={value ?? []}
|
||||
onMoveLayer={onMoveLayer}
|
||||
onEditLayer={onEditClick}
|
||||
onRemoveLayer={onRemoveClick}
|
||||
onAddLayer={onAddClick}
|
||||
draggable={!popoverVisible}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayerConfigsControl;
|
||||
@@ -0,0 +1,506 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { css, JsonValue, styled, t } from '@superset-ui/core';
|
||||
import { Button, Form, Tabs } from 'antd';
|
||||
import { mix } from 'polished';
|
||||
import { Data as GsData } from 'geostyler-data';
|
||||
import { Style as GsStyle } from 'geostyler-style';
|
||||
import WfsDataParser, {
|
||||
RequestParams1_1_0,
|
||||
RequestParams2_0_0,
|
||||
} from 'geostyler-wfs-parser';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { isWfsLayerConf, isWmsLayerConf, isXyzLayerConf } from './typeguards';
|
||||
import {
|
||||
BaseLayerConf,
|
||||
LayerConf,
|
||||
LayerConfigsPopoverContentProps,
|
||||
WfsLayerConf,
|
||||
WmsLayerConf,
|
||||
XyzLayerConf,
|
||||
} from './types';
|
||||
import { getServiceVersions, hasAllRequiredWfsParams } from './serviceUtil';
|
||||
import { ControlFormItem } from '../ColumnConfigControl/ControlForm';
|
||||
import GeoStylerWrapper from './GeoStylerWrapper';
|
||||
|
||||
// Enum for the different tabs
|
||||
const LAYER_CONFIG_TABS = {
|
||||
LAYER: '1',
|
||||
GEOSTYLER: '2',
|
||||
};
|
||||
|
||||
export const StyledButtonContainer = styled.div`
|
||||
display: flex;
|
||||
margin: 8px;
|
||||
`;
|
||||
|
||||
export const StyledCloseButton = styled(Button)`
|
||||
${({ theme }) => css`
|
||||
flex: 1;
|
||||
margin-right: 4px;
|
||||
line-height: 1.5715;
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
background-color: ${theme.colors.primary.light4};
|
||||
color: ${theme.colors.primary.dark1};
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
font-weight: ${theme.typography.weights.bold};
|
||||
text-transform: uppercase;
|
||||
min-width: ${theme.gridUnit * 36};
|
||||
min-height: ${theme.gridUnit * 8};
|
||||
box-shadow: none;
|
||||
border-width: 0px;
|
||||
border-style: none;
|
||||
border-color: transparent;
|
||||
&:hover {
|
||||
background-color: ${mix(
|
||||
0.1,
|
||||
theme.colors.primary.base,
|
||||
theme.colors.primary.light4,
|
||||
)};
|
||||
color: ${theme.colors.primary.dark1};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledControlFormItem = styled(ControlFormItem)`
|
||||
${({ theme }) => css`
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledControlNumberFormItem = styled(ControlFormItem)`
|
||||
${({ theme }) => css`
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
width: 100%;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledGeoStyler = styled(GeoStylerWrapper)`
|
||||
${({ theme }) => css`
|
||||
h2 {
|
||||
font-weight: ${theme.typography.weights.normal};
|
||||
font-size: ${theme.typography.sizes.xl}px;
|
||||
}
|
||||
.ant-form-item-control {
|
||||
flex: unset;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledSaveButton = styled(Button)`
|
||||
${({ theme }) => css`
|
||||
flex: 1;
|
||||
margin-left: 4px;
|
||||
line-height: 1.5715;
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
background-color: ${theme.colors.primary.base};
|
||||
color: ${theme.colors.grayscale.light5};
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
font-weight: ${theme.typography.weights.bold};
|
||||
text-transform: uppercase;
|
||||
min-width: ${theme.gridUnit * 36};
|
||||
min-height: ${theme.gridUnit * 8};
|
||||
box-shadow: none;
|
||||
border-width: 0px;
|
||||
border-style: none;
|
||||
border-color: transparent;
|
||||
&:hover {
|
||||
background-color: ${theme.colors.primary.dark1};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const LayerConfigsPopoverContent: FC<
|
||||
LayerConfigsPopoverContentProps
|
||||
> = ({ onClose = () => {}, onSave = () => {}, layerConf }) => {
|
||||
const [currentLayerConf, setCurrentLayerConf] =
|
||||
useState<LayerConf>(layerConf);
|
||||
const initialWmsVersion =
|
||||
layerConf.type === 'WMS' ? layerConf.version : undefined;
|
||||
const [wmsVersion, setWmsVersion] = useState<string | undefined>(
|
||||
initialWmsVersion,
|
||||
);
|
||||
const initialWfsVersion =
|
||||
layerConf.type === 'WFS' ? layerConf.version : undefined;
|
||||
const [wfsVersion, setWfsVersion] = useState<string | undefined>(
|
||||
initialWfsVersion,
|
||||
);
|
||||
const [geostylerData, setGeoStylerData] = useState<GsData | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const serviceVersions = getServiceVersions();
|
||||
|
||||
// This is needed to force mounting the form every time
|
||||
// we get a new layerConf prop. Otherwise the input fields
|
||||
// will not be updated properly, since ControlFormItem only
|
||||
// recognises the `value` property once and then handles the
|
||||
// values in its on state. Remounting creates a new component
|
||||
// and thereby starts with a fresh state.
|
||||
const [formKey, setFormKey] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentLayerConf({ ...layerConf });
|
||||
setFormKey(oldFormKey => oldFormKey + 1);
|
||||
}, [layerConf]);
|
||||
|
||||
const onFieldValueChange = (value: JsonValue, key: string) => {
|
||||
setCurrentLayerConf({
|
||||
...currentLayerConf,
|
||||
[key]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const onLayerTypeChange = (value: LayerConf['type']) => {
|
||||
if (value === 'WFS') {
|
||||
setCurrentLayerConf({
|
||||
...currentLayerConf,
|
||||
type: value,
|
||||
version: serviceVersions[value][0],
|
||||
style: {
|
||||
name: 'Default Style',
|
||||
rules: [
|
||||
{
|
||||
name: 'Default Rule',
|
||||
symbolizers: [
|
||||
{
|
||||
kind: 'Line',
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
color: '#000000',
|
||||
width: 2,
|
||||
},
|
||||
{
|
||||
kind: 'Mark',
|
||||
wellKnownName: 'circle',
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
color: '#000000',
|
||||
},
|
||||
{
|
||||
kind: 'Fill',
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
color: '#000000',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as WfsLayerConf);
|
||||
} else if (value === 'XYZ') {
|
||||
setCurrentLayerConf({
|
||||
...currentLayerConf,
|
||||
type: value,
|
||||
} as XyzLayerConf);
|
||||
} else {
|
||||
setCurrentLayerConf({
|
||||
...currentLayerConf,
|
||||
type: value,
|
||||
version: serviceVersions[value][0],
|
||||
} as WmsLayerConf);
|
||||
}
|
||||
};
|
||||
|
||||
const onLayerTitleChange = (fieldValue: string) => {
|
||||
onFieldValueChange(fieldValue, 'title');
|
||||
};
|
||||
|
||||
const onLayerUrlChange = (fieldValue: string) => {
|
||||
onFieldValueChange(fieldValue, 'url');
|
||||
};
|
||||
|
||||
const onLayersParamChange = (fieldValue: string) => {
|
||||
onFieldValueChange(fieldValue, 'layersParam');
|
||||
};
|
||||
|
||||
const onTypeNameChange = (fieldValue: string) => {
|
||||
onFieldValueChange(fieldValue, 'typeName');
|
||||
};
|
||||
|
||||
const onWmsVersionChange = (fieldValue: string) => {
|
||||
onFieldValueChange(fieldValue, 'version');
|
||||
setWmsVersion(fieldValue);
|
||||
};
|
||||
|
||||
const onWfsVersionChange = (fieldValue: string) => {
|
||||
onFieldValueChange(fieldValue, 'version');
|
||||
setWfsVersion(fieldValue);
|
||||
};
|
||||
|
||||
const onMaxFeaturesChange = (fieldValue: number) => {
|
||||
onFieldValueChange(fieldValue, 'maxFeatures');
|
||||
};
|
||||
|
||||
const onStyleChange = (fieldValue: GsStyle) => {
|
||||
onFieldValueChange(fieldValue, 'style');
|
||||
};
|
||||
|
||||
const onAttributionChange = (fieldValue: string) => {
|
||||
onFieldValueChange(fieldValue, 'attribution');
|
||||
};
|
||||
|
||||
const onCloseClick = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onSaveClick = () => {
|
||||
const baseConfs: BaseLayerConf = {
|
||||
title: currentLayerConf.title,
|
||||
url: currentLayerConf.url,
|
||||
type: currentLayerConf.type,
|
||||
attribution: currentLayerConf.attribution,
|
||||
};
|
||||
|
||||
let conf: LayerConf;
|
||||
if (isWmsLayerConf(currentLayerConf)) {
|
||||
conf = {
|
||||
...baseConfs,
|
||||
version: currentLayerConf.version,
|
||||
type: currentLayerConf.type,
|
||||
layersParam: currentLayerConf.layersParam,
|
||||
};
|
||||
} else if (isXyzLayerConf(currentLayerConf)) {
|
||||
conf = {
|
||||
...baseConfs,
|
||||
type: currentLayerConf.type,
|
||||
};
|
||||
} else {
|
||||
conf = {
|
||||
...baseConfs,
|
||||
type: currentLayerConf.type,
|
||||
version: currentLayerConf.version,
|
||||
typeName: currentLayerConf.typeName,
|
||||
maxFeatures: currentLayerConf.maxFeatures,
|
||||
style: currentLayerConf.style,
|
||||
};
|
||||
}
|
||||
|
||||
onSave(conf);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isWfsLayerConf(currentLayerConf) ||
|
||||
!hasAllRequiredWfsParams(currentLayerConf)
|
||||
) {
|
||||
setGeoStylerData(undefined);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const readWfsData = async (conf: WfsLayerConf) => {
|
||||
const wfsParser = new WfsDataParser();
|
||||
try {
|
||||
let requestParams: RequestParams1_1_0 | RequestParams2_0_0 = {} as
|
||||
| RequestParams1_1_0
|
||||
| RequestParams2_0_0;
|
||||
if (conf.version.startsWith('1.')) {
|
||||
requestParams = {
|
||||
version: conf.version as RequestParams1_1_0['version'],
|
||||
maxFeatures: conf.maxFeatures,
|
||||
typeName: conf.typeName,
|
||||
};
|
||||
}
|
||||
if (conf.version.startsWith('2.')) {
|
||||
requestParams = {
|
||||
version: conf.version as RequestParams2_0_0['version'],
|
||||
count: conf.maxFeatures,
|
||||
typeNames: conf.typeName,
|
||||
};
|
||||
}
|
||||
|
||||
const gsData = await wfsParser.readData({
|
||||
url: conf.url,
|
||||
requestParams,
|
||||
});
|
||||
setGeoStylerData(gsData);
|
||||
} catch {
|
||||
console.warn('Could not read geostyler data');
|
||||
setGeoStylerData(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
// debounce function
|
||||
const timer = setTimeout(() => readWfsData(currentLayerConf), 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [currentLayerConf]);
|
||||
|
||||
const layerTabLabel = t('Layer');
|
||||
const styleTabLabel = t('Style');
|
||||
const layerTypeLabel = t('Layer type');
|
||||
const layerTypeDescription = t('The type of the layer');
|
||||
const serviceVersionLabel = t('Service version');
|
||||
const serviceVersionDescription = t('The version of the service');
|
||||
const layersParamLabel = t('Layer Name');
|
||||
const layersParamDescription = t(
|
||||
'The name of the layer as described in GetCapabilities',
|
||||
);
|
||||
const layersParamPlaceholder = t('Layer Name');
|
||||
const layerTitleLabel = t('Layer title');
|
||||
const layerTitleDescription = t('The visible title of the layer');
|
||||
const layerTitlePlaceholder = t('Insert Layer title');
|
||||
const layerUrlLabel = t('Layer URL');
|
||||
const layerUrlDescription = t('The service url of the layer');
|
||||
const layerUrlPlaceholder = t('Insert Layer URL');
|
||||
const maxFeaturesLabel = t('Max. features');
|
||||
const maxFeaturesDescription = t(
|
||||
'Maximum number of features to fetch from service',
|
||||
);
|
||||
const maxFeaturesPlaceholder = t('10000');
|
||||
const attributionLabel = t('Attribution');
|
||||
const attributionDescription = t('The layer attribution');
|
||||
const attributionPlaceholder = t('© Layer attribution');
|
||||
|
||||
const wmsVersionOptions: { value: any; label: string }[] =
|
||||
serviceVersions.WMS.map(version => ({ value: version, label: version }));
|
||||
const wfsVersionOptions: { value: any; label: string }[] =
|
||||
serviceVersions.WFS.map(version => ({ value: version, label: version }));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form key={JSON.stringify(formKey)}>
|
||||
<Tabs defaultActiveKey={LAYER_CONFIG_TABS.LAYER}>
|
||||
<Tabs.TabPane tab={layerTabLabel} key={LAYER_CONFIG_TABS.LAYER}>
|
||||
<StyledControlFormItem
|
||||
controlType="Input"
|
||||
label={layerUrlLabel}
|
||||
description={layerUrlDescription}
|
||||
placeholder={layerUrlPlaceholder}
|
||||
value={currentLayerConf.url}
|
||||
name="url"
|
||||
onChange={onLayerUrlChange}
|
||||
/>
|
||||
<StyledControlFormItem
|
||||
controlType="Select"
|
||||
label={layerTypeLabel}
|
||||
description={layerTypeDescription}
|
||||
options={[
|
||||
{ value: 'WMS', label: t('WMS') },
|
||||
{ value: 'WFS', label: t('WFS') },
|
||||
{ value: 'XYZ', label: t('XYZ') },
|
||||
]}
|
||||
value={currentLayerConf.type}
|
||||
defaultValue={currentLayerConf.type}
|
||||
name="type"
|
||||
onChange={onLayerTypeChange}
|
||||
/>
|
||||
{isWmsLayerConf(currentLayerConf) && (
|
||||
<StyledControlFormItem
|
||||
controlType="Select"
|
||||
label={serviceVersionLabel}
|
||||
description={serviceVersionDescription}
|
||||
options={wmsVersionOptions}
|
||||
value={wmsVersion}
|
||||
defaultValue={wmsVersionOptions[0].value as string}
|
||||
name="wmsVersion"
|
||||
onChange={onWmsVersionChange}
|
||||
/>
|
||||
)}
|
||||
{isWfsLayerConf(currentLayerConf) && (
|
||||
<StyledControlFormItem
|
||||
controlType="Select"
|
||||
label={serviceVersionLabel}
|
||||
description={serviceVersionDescription}
|
||||
options={wfsVersionOptions}
|
||||
value={wfsVersion}
|
||||
defaultValue={wfsVersionOptions[0].value as string}
|
||||
name="wfsVersion"
|
||||
onChange={onWfsVersionChange}
|
||||
/>
|
||||
)}
|
||||
{isWmsLayerConf(currentLayerConf) && (
|
||||
<StyledControlFormItem
|
||||
controlType="Input"
|
||||
label={layersParamLabel}
|
||||
description={layersParamDescription}
|
||||
placeholder={layersParamPlaceholder}
|
||||
value={currentLayerConf.layersParam}
|
||||
name="layersParam"
|
||||
onChange={onLayersParamChange}
|
||||
/>
|
||||
)}
|
||||
{isWfsLayerConf(currentLayerConf) && (
|
||||
<StyledControlFormItem
|
||||
controlType="Input"
|
||||
label={layersParamLabel}
|
||||
description={layersParamDescription}
|
||||
placeholder={layersParamPlaceholder}
|
||||
value={currentLayerConf.typeName}
|
||||
name="typeName"
|
||||
onChange={onTypeNameChange}
|
||||
/>
|
||||
)}
|
||||
<StyledControlFormItem
|
||||
controlType="Input"
|
||||
label={layerTitleLabel}
|
||||
description={layerTitleDescription}
|
||||
placeholder={layerTitlePlaceholder}
|
||||
value={currentLayerConf.title}
|
||||
name="title"
|
||||
onChange={onLayerTitleChange}
|
||||
/>
|
||||
{isWfsLayerConf(currentLayerConf) && (
|
||||
<StyledControlNumberFormItem
|
||||
controlType="InputNumber"
|
||||
label={maxFeaturesLabel}
|
||||
description={maxFeaturesDescription}
|
||||
placeholder={maxFeaturesPlaceholder}
|
||||
value={currentLayerConf.maxFeatures}
|
||||
name="maxFeatures"
|
||||
onChange={onMaxFeaturesChange}
|
||||
/>
|
||||
)}
|
||||
<StyledControlFormItem
|
||||
controlType="Input"
|
||||
label={attributionLabel}
|
||||
description={attributionDescription}
|
||||
placeholder={attributionPlaceholder}
|
||||
value={currentLayerConf.attribution}
|
||||
name="attribution"
|
||||
onChange={onAttributionChange}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
tab={styleTabLabel}
|
||||
key={LAYER_CONFIG_TABS.GEOSTYLER}
|
||||
disabled={!isWfsLayerConf(currentLayerConf)}
|
||||
>
|
||||
{isWfsLayerConf(currentLayerConf) && (
|
||||
<StyledGeoStyler
|
||||
style={currentLayerConf.style}
|
||||
onStyleChange={onStyleChange}
|
||||
data={geostylerData}
|
||||
/>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<StyledButtonContainer>
|
||||
<StyledCloseButton type="default" onClick={onCloseClick}>
|
||||
{t('Close')}
|
||||
</StyledCloseButton>
|
||||
<StyledSaveButton type="primary" onClick={onSaveClick}>
|
||||
{t('Save')}
|
||||
</StyledSaveButton>
|
||||
</StyledButtonContainer>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayerConfigsPopoverContent;
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { CloseOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { Button, Tag } from 'antd';
|
||||
import { FC } from 'react';
|
||||
import { LayerTreeItemProps } from './types';
|
||||
|
||||
export const LayerTreeItem: FC<LayerTreeItemProps> = ({
|
||||
layerConf,
|
||||
onEditClick = () => {},
|
||||
onRemoveClick = () => {},
|
||||
className,
|
||||
}) => {
|
||||
const onCloseTag = () => {
|
||||
onRemoveClick();
|
||||
};
|
||||
|
||||
const onEditTag = () => {
|
||||
onEditClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag className={className}>
|
||||
<Button
|
||||
className="layer-tree-item-close"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={onCloseTag}
|
||||
size="small"
|
||||
/>
|
||||
<span
|
||||
className="layer-tree-item-type"
|
||||
onClick={onEditTag}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{layerConf.type}
|
||||
</span>
|
||||
<span
|
||||
className="layer-tree-item-title"
|
||||
onClick={onEditTag}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{layerConf.title}
|
||||
</span>
|
||||
<Button
|
||||
className="layer-tree-item-edit"
|
||||
icon={<RightOutlined />}
|
||||
onClick={onEditTag}
|
||||
size="small"
|
||||
/>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayerTreeItem;
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TreeProps } from 'antd/lib/tree';
|
||||
import { DropInfoType, FlatLayerDataNode } from './types';
|
||||
|
||||
/**
|
||||
* Util for drag and drop related operations.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle drop of flat antd tree.
|
||||
*
|
||||
* Functionality is roughly based on antd tree examples:
|
||||
* https://ant.design/components/tree/
|
||||
*
|
||||
* @param info The argument of the onDrop callback.
|
||||
* @param treeData The list of DataNodes on which the drop event occurred.
|
||||
* @returns A copy of the list with the new sorting.
|
||||
*/
|
||||
export const handleDrop = (
|
||||
info: DropInfoType<TreeProps['onDrop']>,
|
||||
treeData: FlatLayerDataNode[],
|
||||
) => {
|
||||
if (info === undefined) {
|
||||
return [...treeData];
|
||||
}
|
||||
|
||||
const dropKey = info.node.key;
|
||||
const dragKey = info.dragNode.key;
|
||||
const dropPos = info.node.pos.split('-');
|
||||
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
|
||||
|
||||
const data = [...treeData];
|
||||
|
||||
const dragObjIndex = data.findIndex(d => d.key === dragKey);
|
||||
const dragObj = data[dragObjIndex];
|
||||
data.splice(dragObjIndex, 1);
|
||||
|
||||
const dropObjIndex = data.findIndex(d => d.key === dropKey);
|
||||
if (dropPosition === -1) {
|
||||
data.splice(dropObjIndex, 0, dragObj!);
|
||||
} else {
|
||||
data.splice(dropObjIndex + 1, 0, dragObj!);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { WfsLayerConf } from './types';
|
||||
|
||||
/**
|
||||
* Get the available versions of WFS and WMS.
|
||||
*
|
||||
* @returns the versions
|
||||
*/
|
||||
export const getServiceVersions = () => ({
|
||||
WMS: ['1.3.0', '1.1.1'],
|
||||
WFS: ['2.0.2', '2.0.0', '1.1.0'],
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks if all required WFS params are provided.
|
||||
*
|
||||
* @param layerConf The config to check
|
||||
* @returns True, if all required params are provided. False, otherwise.
|
||||
*/
|
||||
export const hasAllRequiredWfsParams = (layerConf: WfsLayerConf) =>
|
||||
layerConf.url && layerConf.version && layerConf.typeName;
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { LayerConf, WfsLayerConf, WmsLayerConf, XyzLayerConf } from './types';
|
||||
|
||||
export const isWmsLayerConf = (
|
||||
layerConf: LayerConf,
|
||||
): layerConf is WmsLayerConf => layerConf.type === 'WMS';
|
||||
|
||||
export const isWfsLayerConf = (
|
||||
layerConf: LayerConf,
|
||||
): layerConf is WfsLayerConf => layerConf.type === 'WFS';
|
||||
|
||||
export const isXyzLayerConf = (
|
||||
layerConf: LayerConf,
|
||||
): layerConf is XyzLayerConf => layerConf.type === 'XYZ';
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { DataNode, TreeProps } from 'antd/lib/tree';
|
||||
import { ControlComponentProps } from '@superset-ui/chart-controls';
|
||||
import { Style } from 'geostyler-style';
|
||||
import { CardStyleProps } from 'geostyler/dist/Component/CardStyle/CardStyle';
|
||||
|
||||
export interface BaseLayerConf {
|
||||
title: string;
|
||||
url: string;
|
||||
type: string;
|
||||
attribution?: string;
|
||||
}
|
||||
|
||||
export interface WfsLayerConf extends BaseLayerConf {
|
||||
type: 'WFS';
|
||||
typeName: string;
|
||||
version: string;
|
||||
maxFeatures?: number;
|
||||
style?: Style;
|
||||
}
|
||||
|
||||
export interface XyzLayerConf extends BaseLayerConf {
|
||||
type: 'XYZ';
|
||||
}
|
||||
|
||||
export interface WmsLayerConf extends BaseLayerConf {
|
||||
type: 'WMS';
|
||||
version: string;
|
||||
layersParam: string;
|
||||
}
|
||||
|
||||
export interface FlatLayerDataNode extends DataNode {
|
||||
layerConf: LayerConf;
|
||||
}
|
||||
|
||||
export interface FlatLayerTreeProps {
|
||||
layerConfigs: LayerConf[];
|
||||
onAddLayer?: () => void;
|
||||
onRemoveLayer?: (idx: number) => void;
|
||||
onEditLayer?: (layerConf: LayerConf, idx: number) => void;
|
||||
onMoveLayer?: (layerConfigs: LayerConf[]) => void;
|
||||
draggable?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type LayerConf = WmsLayerConf | WfsLayerConf | XyzLayerConf;
|
||||
|
||||
export type DropInfoType<T extends TreeProps['onDrop']> = T extends Function
|
||||
? Parameters<T>[0]
|
||||
: undefined;
|
||||
|
||||
export interface EditItem {
|
||||
layerConf: LayerConf;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
export type LayerConfigsControlProps = ControlComponentProps<LayerConf[]>;
|
||||
|
||||
export interface LayerConfigsPopoverContentProps {
|
||||
onClose?: () => void;
|
||||
onSave?: (layerConf: LayerConf) => void;
|
||||
layerConf: LayerConf;
|
||||
}
|
||||
|
||||
export interface GeoStylerWrapperProps extends CardStyleProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface LayerTreeItemProps {
|
||||
layerConf: LayerConf;
|
||||
onEditClick?: () => void;
|
||||
onRemoveClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user