mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: custom layer naming (#41491)
This commit is contained in:
parent
a3b9c764d3
commit
a992447a45
@ -49,7 +49,7 @@ export interface FrameGeometrySource {
|
|||||||
*/
|
*/
|
||||||
export interface MapLayerOptions<TConfig = any> {
|
export interface MapLayerOptions<TConfig = any> {
|
||||||
type: string;
|
type: string;
|
||||||
name?: string; // configured display name
|
name?: string; // configured unique display name
|
||||||
|
|
||||||
// Custom options depending on the type
|
// Custom options depending on the type
|
||||||
config?: TConfig;
|
config?: TConfig;
|
||||||
|
@ -46,6 +46,7 @@ interface State extends OverlayProps {
|
|||||||
export interface GeomapLayerActions {
|
export interface GeomapLayerActions {
|
||||||
selectLayer: (uid: string) => void;
|
selectLayer: (uid: string) => void;
|
||||||
deleteLayer: (uid: string) => void;
|
deleteLayer: (uid: string) => void;
|
||||||
|
updateLayer: (uid: string, updatedLayer: MapLayerState<any>) => void;
|
||||||
addlayer: (type: string) => void;
|
addlayer: (type: string) => void;
|
||||||
reorder: (src: number, dst: number) => void;
|
reorder: (src: number, dst: number) => void;
|
||||||
}
|
}
|
||||||
@ -92,7 +93,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props) {
|
shouldComponentUpdate(nextProps: Props) {
|
||||||
if (!this.map) {
|
if (!this.map) {
|
||||||
return true; // not yet initalized
|
return true; // not yet initialized
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for resize
|
// Check for resize
|
||||||
@ -152,6 +153,21 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
this.layers = layers;
|
this.layers = layers;
|
||||||
this.doOptionsUpdate(0);
|
this.doOptionsUpdate(0);
|
||||||
},
|
},
|
||||||
|
updateLayer: (uid: string, updatedLayer: MapLayerState<any>) => {
|
||||||
|
const selected = this.layers.findIndex((v) => v.UID === uid);
|
||||||
|
const layers: MapLayerState[] = [];
|
||||||
|
for (const lyr of this.layers) {
|
||||||
|
if (lyr.UID === uid) {
|
||||||
|
this.map?.removeLayer(lyr.layer);
|
||||||
|
this.map?.addLayer(updatedLayer.layer);
|
||||||
|
layers.push(updatedLayer);
|
||||||
|
} else {
|
||||||
|
layers.push(lyr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.layers = layers;
|
||||||
|
this.doOptionsUpdate(selected);
|
||||||
|
},
|
||||||
addlayer: (type: string) => {
|
addlayer: (type: string) => {
|
||||||
const item = geomapLayerRegistry.getIfExists(type);
|
const item = geomapLayerRegistry.getIfExists(type);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
@ -361,7 +377,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
layers[selected] = info;
|
layers[selected] = info;
|
||||||
|
|
||||||
// initalize with new data
|
// initialize with new data
|
||||||
if (info.handler.update) {
|
if (info.handler.update) {
|
||||||
info.handler.update(this.props.data);
|
info.handler.update(this.props.data);
|
||||||
}
|
}
|
||||||
@ -378,6 +394,18 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private generateLayerName = (): string => {
|
||||||
|
let newLayerName = `Layer ${this.counter}`;
|
||||||
|
|
||||||
|
for (const otherLayer of this.layers) {
|
||||||
|
if (newLayerName === otherLayer.options.name) {
|
||||||
|
newLayerName += '-1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newLayerName;
|
||||||
|
};
|
||||||
|
|
||||||
async initLayer(map: Map, options: MapLayerOptions, isBasemap?: boolean): Promise<MapLayerState> {
|
async initLayer(map: Map, options: MapLayerOptions, isBasemap?: boolean): Promise<MapLayerState> {
|
||||||
if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) {
|
if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) {
|
||||||
options = DEFAULT_BASEMAP_CONFIG;
|
options = DEFAULT_BASEMAP_CONFIG;
|
||||||
@ -408,7 +436,11 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
handler.update(this.props.data);
|
handler.update(this.props.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!options.name) {
|
||||||
|
options.name = this.generateLayerName();
|
||||||
|
}
|
||||||
const UID = `lyr-${this.counter++}`;
|
const UID = `lyr-${this.counter++}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
UID,
|
UID,
|
||||||
isBasemap,
|
isBasemap,
|
||||||
@ -427,7 +459,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
let view = new View({
|
let view = new View({
|
||||||
center: [0, 0],
|
center: [0, 0],
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
showFullExtent: true, // alows zooming so the full range is visiable
|
showFullExtent: true, // allows zooming so the full range is visible
|
||||||
});
|
});
|
||||||
|
|
||||||
// With shared views, all panels use the same view instance
|
// With shared views, all panels use the same view instance
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import { cx } from '@emotion/css';
|
|
||||||
import { Container, Icon, IconButton, ValuePicker } from '@grafana/ui';
|
|
||||||
import { StandardEditorProps } from '@grafana/data';
|
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
|
||||||
|
|
||||||
import { GeomapPanelOptions } from '../types';
|
|
||||||
import { GeomapInstanceState } from '../GeomapPanel';
|
|
||||||
import { geomapLayerRegistry } from '../layers/registry';
|
|
||||||
import { getLayerDragStyles } from '../../canvas/editor/LayerElementListEditor';
|
|
||||||
import { dataLayerFilter } from './layerEditor';
|
|
||||||
|
|
||||||
type Props = StandardEditorProps<any, any, GeomapPanelOptions, GeomapInstanceState>;
|
|
||||||
|
|
||||||
export class LayersEditor extends PureComponent<Props> {
|
|
||||||
style = getLayerDragStyles(config.theme);
|
|
||||||
|
|
||||||
getRowStyle = (sel: boolean) => {
|
|
||||||
return sel ? `${this.style.row} ${this.style.sel}` : this.style.row;
|
|
||||||
};
|
|
||||||
|
|
||||||
onDragEnd = (result: DropResult) => {
|
|
||||||
if (!result.destination) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { layers, actions } = this.props.context.instanceState ?? {};
|
|
||||||
if (!layers || !actions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// account for the reverse order and offset (0 is baselayer)
|
|
||||||
const count = layers.length - 1;
|
|
||||||
const src = (result.source.index - count) * -1;
|
|
||||||
const dst = (result.destination.index - count) * -1;
|
|
||||||
|
|
||||||
actions.reorder(src, dst);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { layers, selected, actions } = this.props.context.instanceState ?? {};
|
|
||||||
if (!layers || !actions) {
|
|
||||||
return <div>No layers?</div>;
|
|
||||||
}
|
|
||||||
const baselayer = layers[0];
|
|
||||||
|
|
||||||
const styles = this.style;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Container>
|
|
||||||
<ValuePicker
|
|
||||||
icon="plus"
|
|
||||||
label="Add layer"
|
|
||||||
variant="secondary"
|
|
||||||
options={geomapLayerRegistry.selectOptions(undefined, dataLayerFilter).options}
|
|
||||||
onChange={(v) => actions.addlayer(v.value!)}
|
|
||||||
isFullWidth={true}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<DragDropContext onDragEnd={this.onDragEnd}>
|
|
||||||
<Droppable droppableId="droppable">
|
|
||||||
{(provided, snapshot) => (
|
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
|
||||||
{(() => {
|
|
||||||
// reverse order
|
|
||||||
const rows: any = [];
|
|
||||||
for (let i = layers.length - 1; i > 0; i--) {
|
|
||||||
const element = layers[i];
|
|
||||||
rows.push(
|
|
||||||
<Draggable key={element.UID} draggableId={element.UID} index={rows.length}>
|
|
||||||
{(provided, snapshot) => (
|
|
||||||
<div
|
|
||||||
className={this.getRowStyle(i === selected)}
|
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.draggableProps}
|
|
||||||
{...provided.dragHandleProps}
|
|
||||||
onMouseDown={() => actions!.selectLayer(element.UID)}
|
|
||||||
>
|
|
||||||
<span className={styles.typeWrapper}>{element.options.type}</span>
|
|
||||||
<div className={styles.textWrapper}> ({element.layer.getSourceState() ?? '?'})</div>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
name="trash-alt"
|
|
||||||
title={'remove'}
|
|
||||||
className={cx(styles.actionIcon, styles.dragIcon)}
|
|
||||||
onClick={() => actions.deleteLayer(element.UID)}
|
|
||||||
surface="header"
|
|
||||||
/>
|
|
||||||
{layers.length > 2 && (
|
|
||||||
<Icon
|
|
||||||
title="Drag and drop to reorder"
|
|
||||||
name="draggabledots"
|
|
||||||
size="lg"
|
|
||||||
className={styles.dragIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{provided.placeholder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
|
|
||||||
{false && baselayer && (
|
|
||||||
<>
|
|
||||||
<label>Base layer</label>
|
|
||||||
<div className={this.getRowStyle(false)}>
|
|
||||||
<span className={styles.typeWrapper}>{baselayer.options.type}</span>
|
|
||||||
<div className={styles.textWrapper}> {baselayer.UID}</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ValuePicker } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { geomapLayerRegistry } from '../../layers/registry';
|
||||||
|
import { dataLayerFilter } from '../layerEditor';
|
||||||
|
import { GeomapLayerActions } from '../../GeomapPanel';
|
||||||
|
|
||||||
|
type AddLayerButtonProps = { actions: GeomapLayerActions };
|
||||||
|
|
||||||
|
export const AddLayerButton = ({ actions }: AddLayerButtonProps) => {
|
||||||
|
return (
|
||||||
|
<ValuePicker
|
||||||
|
icon="plus"
|
||||||
|
label="Add layer"
|
||||||
|
variant="secondary"
|
||||||
|
options={geomapLayerRegistry.selectOptions(undefined, dataLayerFilter).options}
|
||||||
|
onChange={(v) => actions.addlayer(v.value!)}
|
||||||
|
isFullWidth={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { LayerHeaderProps, LayerHeader } from './LayerHeader';
|
||||||
|
|
||||||
|
describe('LayerHeader', () => {
|
||||||
|
it('Can edit title', () => {
|
||||||
|
const scenario = renderScenario({});
|
||||||
|
screen.getByTestId('layer-name-div').click();
|
||||||
|
|
||||||
|
const input = screen.getByTestId('layer-name-input');
|
||||||
|
fireEvent.change(input, { target: { value: 'new name' } });
|
||||||
|
fireEvent.blur(input);
|
||||||
|
|
||||||
|
expect((scenario.props.onChange as any).mock.calls[0][0].options.name).toBe('new name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Show error when empty name is specified', async () => {
|
||||||
|
renderScenario({});
|
||||||
|
|
||||||
|
screen.getByTestId('layer-name-div').click();
|
||||||
|
const input = screen.getByTestId('layer-name-input');
|
||||||
|
fireEvent.change(input, { target: { value: '' } });
|
||||||
|
const alert = await screen.findByRole('alert');
|
||||||
|
|
||||||
|
expect(alert.textContent).toBe('An empty layer name is not allowed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Show error when other layer with same name exists', async () => {
|
||||||
|
renderScenario({});
|
||||||
|
|
||||||
|
screen.getByTestId('layer-name-div').click();
|
||||||
|
const input = screen.getByTestId('layer-name-input');
|
||||||
|
fireEvent.change(input, { target: { value: 'Layer 2' } });
|
||||||
|
const alert = await screen.findByRole('alert');
|
||||||
|
|
||||||
|
expect(alert.textContent).toBe('Layer name already exists');
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderScenario(overrides: Partial<LayerHeaderProps>) {
|
||||||
|
const props: any = {
|
||||||
|
layer: {
|
||||||
|
UID: '1',
|
||||||
|
options: { name: 'Layer 1' },
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
UID: '1',
|
||||||
|
options: { name: 'Layer 1' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: '2',
|
||||||
|
options: { name: 'Layer 2' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, overrides);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props,
|
||||||
|
renderResult: render(<LayerHeader {...props} />),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,164 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { Icon, Input, FieldValidationMessage, useStyles } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
|
import { MapLayerState } from '../../types';
|
||||||
|
|
||||||
|
export interface LayerHeaderProps {
|
||||||
|
layer: MapLayerState<any>;
|
||||||
|
layers: Array<MapLayerState<any>>;
|
||||||
|
onChange: (layer: MapLayerState<any>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LayerHeader = ({ layer, layers, onChange }: LayerHeaderProps) => {
|
||||||
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const onEditLayer = (event: React.SyntheticEvent) => {
|
||||||
|
setIsEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEndEditName = (newName: string) => {
|
||||||
|
setIsEditing(false);
|
||||||
|
|
||||||
|
if (validationError) {
|
||||||
|
setValidationError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.options.name !== newName) {
|
||||||
|
onChange({
|
||||||
|
...layer,
|
||||||
|
options: { ...layer.options, name: newName },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
|
const newName = event.currentTarget.value.trim();
|
||||||
|
|
||||||
|
if (newName.length === 0) {
|
||||||
|
setValidationError('An empty layer name is not allowed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const otherLayer of layers) {
|
||||||
|
if (otherLayer.UID !== layer.UID && newName === otherLayer.options.name) {
|
||||||
|
setValidationError('Layer name already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationError) {
|
||||||
|
setValidationError(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditLayerBlur = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
|
onEndEditName(event.currentTarget.value.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
onEndEditName((event.target as any).value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
event.target.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
{!isEditing && (
|
||||||
|
<button
|
||||||
|
className={styles.layerNameWrapper}
|
||||||
|
title="Edit layer name"
|
||||||
|
onClick={onEditLayer}
|
||||||
|
data-testid="layer-name-div"
|
||||||
|
>
|
||||||
|
<span className={styles.layerName}>{layer.options.name}</span>
|
||||||
|
<Icon name="pen" className={styles.layerEditIcon} size="sm" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
defaultValue={layer.options.name}
|
||||||
|
onBlur={onEditLayerBlur}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onFocus={onFocus}
|
||||||
|
invalid={validationError !== null}
|
||||||
|
onChange={onInputChange}
|
||||||
|
className={styles.layerNameInput}
|
||||||
|
data-testid="layer-name-input"
|
||||||
|
/>
|
||||||
|
{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => {
|
||||||
|
return {
|
||||||
|
wrapper: css`
|
||||||
|
label: Wrapper;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: ${theme.spacing.xs};
|
||||||
|
`,
|
||||||
|
layerNameWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: ${theme.border.radius.md};
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0 0 ${theme.spacing.xs};
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${theme.colors.bg3};
|
||||||
|
border: 1px dashed ${theme.colors.border3};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 2px solid ${theme.colors.formInputBorderActive};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
.query-name-edit-icon {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
layerName: css`
|
||||||
|
font-weight: ${theme.typography.weight.semibold};
|
||||||
|
color: ${theme.colors.textBlue};
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-left: ${theme.spacing.xs};
|
||||||
|
`,
|
||||||
|
layerEditIcon: cx(
|
||||||
|
css`
|
||||||
|
margin-left: ${theme.spacing.md};
|
||||||
|
visibility: hidden;
|
||||||
|
`,
|
||||||
|
'query-name-edit-icon'
|
||||||
|
),
|
||||||
|
layerNameInput: css`
|
||||||
|
max-width: 300px;
|
||||||
|
margin: -4px 0;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Icon, IconButton } from '@grafana/ui';
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { MapLayerState } from '../../types';
|
||||||
|
import { getLayerDragStyles } from 'app/plugins/panel/canvas/editor/LayerElementListEditor';
|
||||||
|
import { GeomapLayerActions } from '../../GeomapPanel';
|
||||||
|
import { LayerHeader } from './LayerHeader';
|
||||||
|
|
||||||
|
type LayerListProps = {
|
||||||
|
layers: Array<MapLayerState<any>>;
|
||||||
|
onDragEnd: (result: DropResult) => void;
|
||||||
|
selected?: number;
|
||||||
|
actions: GeomapLayerActions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LayerList = ({ layers, onDragEnd, selected, actions }: LayerListProps) => {
|
||||||
|
const style = getLayerDragStyles(config.theme);
|
||||||
|
|
||||||
|
const getRowStyle = (sel: boolean) => {
|
||||||
|
return sel ? `${style.row} ${style.sel}` : style.row;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLayerNameChange = (layer: MapLayerState<any>) => {
|
||||||
|
actions.updateLayer(layer.UID, layer);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable droppableId="droppable">
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
|
{(() => {
|
||||||
|
// reverse order
|
||||||
|
const rows: any = [];
|
||||||
|
for (let i = layers.length - 1; i > 0; i--) {
|
||||||
|
const element = layers[i];
|
||||||
|
rows.push(
|
||||||
|
<Draggable key={element.UID} draggableId={element.UID} index={rows.length}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
className={getRowStyle(i === selected)}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
onMouseDown={() => actions!.selectLayer(element.UID)}
|
||||||
|
>
|
||||||
|
<LayerHeader layer={{ ...element }} layers={layers} onChange={onLayerNameChange} />
|
||||||
|
<div className={style.textWrapper}> {element.options.type}</div>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name="trash-alt"
|
||||||
|
title={'remove'}
|
||||||
|
className={cx(style.actionIcon, style.dragIcon)}
|
||||||
|
onClick={() => actions.deleteLayer(element.UID)}
|
||||||
|
surface="header"
|
||||||
|
/>
|
||||||
|
{layers.length > 2 && (
|
||||||
|
<Icon
|
||||||
|
title="Drag and drop to reorder"
|
||||||
|
name="draggabledots"
|
||||||
|
size="lg"
|
||||||
|
className={style.dragIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Container } from '@grafana/ui';
|
||||||
|
import { StandardEditorProps } from '@grafana/data';
|
||||||
|
import { DropResult } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { GeomapPanelOptions } from '../../types';
|
||||||
|
import { GeomapInstanceState } from '../../GeomapPanel';
|
||||||
|
import { AddLayerButton } from './AddLayerButton';
|
||||||
|
import { LayerList } from './LayerList';
|
||||||
|
|
||||||
|
type LayersEditorProps = StandardEditorProps<any, any, GeomapPanelOptions, GeomapInstanceState>;
|
||||||
|
|
||||||
|
export const LayersEditor = (props: LayersEditorProps) => {
|
||||||
|
const { layers, selected, actions } = props.context.instanceState ?? {};
|
||||||
|
if (!layers || !actions) {
|
||||||
|
return <div>No layers?</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragEnd = (result: DropResult) => {
|
||||||
|
if (!result.destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { layers, actions } = props.context.instanceState ?? {};
|
||||||
|
if (!layers || !actions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// account for the reverse order and offset (0 is baselayer)
|
||||||
|
const count = layers.length - 1;
|
||||||
|
const src = (result.source.index - count) * -1;
|
||||||
|
const dst = (result.destination.index - count) * -1;
|
||||||
|
|
||||||
|
actions.reorder(src, dst);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container>
|
||||||
|
<AddLayerButton actions={actions} />
|
||||||
|
</Container>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<LayerList layers={layers} onDragEnd={onDragEnd} selected={selected} actions={actions} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -5,7 +5,7 @@ import { MapViewEditor } from './editor/MapViewEditor';
|
|||||||
import { defaultView, GeomapPanelOptions } from './types';
|
import { defaultView, GeomapPanelOptions } from './types';
|
||||||
import { mapPanelChangedHandler, mapMigrationHandler } from './migrations';
|
import { mapPanelChangedHandler, mapMigrationHandler } from './migrations';
|
||||||
import { getLayerEditor } from './editor/layerEditor';
|
import { getLayerEditor } from './editor/layerEditor';
|
||||||
import { LayersEditor } from './editor/LayersEditor';
|
import { LayersEditor } from './editor/LayersEditor/LayersEditor';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||||
|
@ -71,7 +71,7 @@ export interface GazetteerPathEditorConfigSettings {
|
|||||||
// Runtime model
|
// Runtime model
|
||||||
//-------------------
|
//-------------------
|
||||||
export interface MapLayerState<TConfig = any> {
|
export interface MapLayerState<TConfig = any> {
|
||||||
UID: string; // value changes with each initalization
|
UID: string; // value changes with each initialization
|
||||||
options: MapLayerOptions<TConfig>;
|
options: MapLayerOptions<TConfig>;
|
||||||
handler: MapLayerHandler;
|
handler: MapLayerHandler;
|
||||||
layer: BaseLayer; // the openlayers instance
|
layer: BaseLayer; // the openlayers instance
|
||||||
|
Loading…
Reference in New Issue
Block a user