mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: refactor layer editor (#42562)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -85,6 +85,7 @@ go.sum @grafana/backend-platform
|
|||||||
/plugins-bundled @grafana/plugins-platform-frontend
|
/plugins-bundled @grafana/plugins-platform-frontend
|
||||||
/public @grafana/user-essentials
|
/public @grafana/user-essentials
|
||||||
/public/app/core/components/TimePicker @grafana/grafana-bi-squad
|
/public/app/core/components/TimePicker @grafana/grafana-bi-squad
|
||||||
|
/public/app/core/components/Layers @grafana/grafana-edge-squad
|
||||||
/public/app/features/canvas/ @grafana/grafana-edge-squad
|
/public/app/features/canvas/ @grafana/grafana-edge-squad
|
||||||
/public/app/features/dimensions/ @grafana/grafana-edge-squad
|
/public/app/features/dimensions/ @grafana/grafana-edge-squad
|
||||||
/public/app/features/live/ @grafana/grafana-edge-squad
|
/public/app/features/live/ @grafana/grafana-edge-squad
|
||||||
|
|||||||
23
public/app/core/components/Layers/AddLayerButton.tsx
Normal file
23
public/app/core/components/Layers/AddLayerButton.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ValuePicker } from '@grafana/ui';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
|
type AddLayerButtonProps = {
|
||||||
|
onChange: (sel: SelectableValue<string>) => void;
|
||||||
|
options: Array<SelectableValue<string>>;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddLayerButton = ({ onChange, options, label }: AddLayerButtonProps) => {
|
||||||
|
return (
|
||||||
|
<ValuePicker
|
||||||
|
icon="plus"
|
||||||
|
label={label}
|
||||||
|
variant="secondary"
|
||||||
|
options={options}
|
||||||
|
onChange={onChange}
|
||||||
|
isFullWidth={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
171
public/app/core/components/Layers/LayerDragDropList.tsx
Normal file
171
public/app/core/components/Layers/LayerDragDropList.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { Icon, IconButton, stylesFactory } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { LayerName } from './LayerName';
|
||||||
|
import { LayerElement } from './types';
|
||||||
|
|
||||||
|
type LayerDragDropListProps<T extends LayerElement> = {
|
||||||
|
layers: T[];
|
||||||
|
getLayerInfo: (element: T) => string;
|
||||||
|
onDragEnd: (result: DropResult) => void;
|
||||||
|
onSelect: (element: T) => any;
|
||||||
|
onDelete: (element: T) => any;
|
||||||
|
onDuplicate?: (element: T) => any;
|
||||||
|
isGroup?: (element: T) => boolean;
|
||||||
|
selection?: string[]; // list of unique ids (names)
|
||||||
|
excludeBaseLayer?: boolean;
|
||||||
|
onNameChange: (element: T, newName: string) => any;
|
||||||
|
verifyLayerNameUniqueness?: (nameToCheck: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LayerDragDropList = <T extends LayerElement>({
|
||||||
|
layers,
|
||||||
|
getLayerInfo,
|
||||||
|
onDragEnd,
|
||||||
|
onSelect,
|
||||||
|
onDelete,
|
||||||
|
onDuplicate,
|
||||||
|
isGroup,
|
||||||
|
selection,
|
||||||
|
excludeBaseLayer,
|
||||||
|
onNameChange,
|
||||||
|
verifyLayerNameUniqueness,
|
||||||
|
}: LayerDragDropListProps<T>) => {
|
||||||
|
const style = styles(config.theme);
|
||||||
|
|
||||||
|
const getRowStyle = (isSelected: boolean) => {
|
||||||
|
return isSelected ? `${style.row} ${style.sel}` : style.row;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable droppableId="droppable">
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
|
{(() => {
|
||||||
|
// reverse order
|
||||||
|
const rows: any = [];
|
||||||
|
const lastLayerIndex = excludeBaseLayer ? 1 : 0;
|
||||||
|
for (let i = layers.length - 1; i >= lastLayerIndex; i--) {
|
||||||
|
const element = layers[i];
|
||||||
|
const uid = element.getName();
|
||||||
|
|
||||||
|
const isSelected = Boolean(selection?.includes(uid));
|
||||||
|
rows.push(
|
||||||
|
<Draggable key={uid} draggableId={uid} index={rows.length}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
className={getRowStyle(isSelected)}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
onMouseDown={() => onSelect(element)}
|
||||||
|
>
|
||||||
|
<LayerName
|
||||||
|
name={uid}
|
||||||
|
onChange={(v) => onNameChange(element, v)}
|
||||||
|
verifyLayerNameUniqueness={verifyLayerNameUniqueness ?? undefined}
|
||||||
|
/>
|
||||||
|
<div className={style.textWrapper}> {getLayerInfo(element)}</div>
|
||||||
|
|
||||||
|
{!isGroup!(element) && (
|
||||||
|
<>
|
||||||
|
{onDuplicate ? (
|
||||||
|
<IconButton
|
||||||
|
name="copy"
|
||||||
|
title={'Duplicate'}
|
||||||
|
className={style.actionIcon}
|
||||||
|
onClick={() => onDuplicate(element)}
|
||||||
|
surface="header"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name="trash-alt"
|
||||||
|
title={'remove'}
|
||||||
|
className={cx(style.actionIcon, style.dragIcon)}
|
||||||
|
onClick={() => onDelete(element)}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LayerDragDropList.defaultProps = {
|
||||||
|
isGroup: () => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = stylesFactory((theme: GrafanaTheme) => ({
|
||||||
|
wrapper: css`
|
||||||
|
margin-bottom: ${theme.spacing.md};
|
||||||
|
`,
|
||||||
|
row: css`
|
||||||
|
padding: ${theme.spacing.xs} ${theme.spacing.sm};
|
||||||
|
border-radius: ${theme.border.radius.sm};
|
||||||
|
background: ${theme.colors.bg2};
|
||||||
|
min-height: ${theme.spacing.formInputHeight}px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
border: 1px solid ${theme.colors.formInputBorder};
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid ${theme.colors.formInputBorderHover};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
sel: css`
|
||||||
|
border: 1px solid ${theme.colors.formInputBorderActive};
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid ${theme.colors.formInputBorderActive};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
dragIcon: css`
|
||||||
|
cursor: drag;
|
||||||
|
`,
|
||||||
|
actionIcon: css`
|
||||||
|
color: ${theme.colors.textWeak};
|
||||||
|
&:hover {
|
||||||
|
color: ${theme.colors.text};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
typeWrapper: css`
|
||||||
|
color: ${theme.colors.textBlue};
|
||||||
|
margin-right: 5px;
|
||||||
|
`,
|
||||||
|
textWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: ${theme.spacing.sm};
|
||||||
|
`,
|
||||||
|
}));
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import { LayerHeaderProps, LayerHeader } from './LayerHeader';
|
import { LayerNameProps, LayerName } from './LayerName';
|
||||||
|
|
||||||
describe('LayerHeader', () => {
|
describe('LayerName', () => {
|
||||||
it('Can edit title', () => {
|
it('Can edit title', () => {
|
||||||
const scenario = renderScenario({});
|
const scenario = renderScenario({});
|
||||||
screen.getByTestId('layer-name-div').click();
|
screen.getByTestId('layer-name-div').click();
|
||||||
@@ -11,7 +11,7 @@ describe('LayerHeader', () => {
|
|||||||
fireEvent.change(input, { target: { value: 'new name' } });
|
fireEvent.change(input, { target: { value: 'new name' } });
|
||||||
fireEvent.blur(input);
|
fireEvent.blur(input);
|
||||||
|
|
||||||
expect((scenario.props.onChange as any).mock.calls[0][0].name).toBe('new name');
|
expect((scenario.props.onChange as any).mock.calls[0][0]).toBe('new name');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Show error when empty name is specified', async () => {
|
it('Show error when empty name is specified', async () => {
|
||||||
@@ -36,21 +36,21 @@ describe('LayerHeader', () => {
|
|||||||
expect(alert.textContent).toBe('Layer name already exists');
|
expect(alert.textContent).toBe('Layer name already exists');
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderScenario(overrides: Partial<LayerHeaderProps>) {
|
function renderScenario(overrides: Partial<LayerNameProps>) {
|
||||||
const props: LayerHeaderProps = {
|
const props: LayerNameProps = {
|
||||||
layer: { name: 'Layer 1', type: '?' },
|
name: 'Layer 1',
|
||||||
canRename: (v: string) => {
|
|
||||||
const names = new Set(['Layer 1', 'Layer 2']);
|
|
||||||
return !names.has(v);
|
|
||||||
},
|
|
||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
|
verifyLayerNameUniqueness: (nameToCheck: string) => {
|
||||||
|
const names = new Set(['Layer 1', 'Layer 2']);
|
||||||
|
return !names.has(nameToCheck);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, overrides);
|
Object.assign(props, overrides);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props,
|
props,
|
||||||
renderResult: render(<LayerHeader {...props} />),
|
renderResult: render(<LayerName {...props} />),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { Icon, Input, FieldValidationMessage, useStyles } from '@grafana/ui';
|
import { Icon, Input, FieldValidationMessage, useStyles } from '@grafana/ui';
|
||||||
import { GrafanaTheme, MapLayerOptions } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
export interface LayerHeaderProps {
|
export interface LayerNameProps {
|
||||||
layer: MapLayerOptions<any>;
|
name: string;
|
||||||
canRename: (v: string) => boolean;
|
onChange: (v: string) => void;
|
||||||
onChange: (layer: MapLayerOptions<any>) => void;
|
verifyLayerNameUniqueness?: (nameToCheck: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LayerHeader = ({ layer, canRename, onChange }: LayerHeaderProps) => {
|
export const LayerName = ({ name, onChange, verifyLayerNameUniqueness }: LayerNameProps) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
@@ -27,11 +27,8 @@ export const LayerHeader = ({ layer, canRename, onChange }: LayerHeaderProps) =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layer.name !== newName) {
|
if (name !== newName) {
|
||||||
onChange({
|
onChange(newName);
|
||||||
...layer,
|
|
||||||
name: newName,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,7 +40,7 @@ export const LayerHeader = ({ layer, canRename, onChange }: LayerHeaderProps) =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canRename(newName)) {
|
if (verifyLayerNameUniqueness && !verifyLayerNameUniqueness(newName)) {
|
||||||
setValidationError('Layer name already exists');
|
setValidationError('Layer name already exists');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,7 +74,7 @@ export const LayerHeader = ({ layer, canRename, onChange }: LayerHeaderProps) =>
|
|||||||
onClick={onEditLayer}
|
onClick={onEditLayer}
|
||||||
data-testid="layer-name-div"
|
data-testid="layer-name-div"
|
||||||
>
|
>
|
||||||
<span className={styles.layerName}>{layer.name}</span>
|
<span className={styles.layerName}>{name}</span>
|
||||||
<Icon name="pen" className={styles.layerEditIcon} size="sm" />
|
<Icon name="pen" className={styles.layerEditIcon} size="sm" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -86,7 +83,7 @@ export const LayerHeader = ({ layer, canRename, onChange }: LayerHeaderProps) =>
|
|||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
defaultValue={layer.name}
|
defaultValue={name}
|
||||||
onBlur={onEditLayerBlur}
|
onBlur={onEditLayerBlur}
|
||||||
autoFocus
|
autoFocus
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
4
public/app/core/components/Layers/types.ts
Normal file
4
public/app/core/components/Layers/types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/** An interface that has a getName method */
|
||||||
|
export interface LayerElement {
|
||||||
|
getName: () => string;
|
||||||
|
}
|
||||||
@@ -12,12 +12,13 @@ import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
|||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export interface CanvasElementOptions<TConfig = any> {
|
export interface CanvasElementOptions<TConfig = any> {
|
||||||
|
name: string; // configured unique display name
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
// Custom options depending on the type
|
// Custom options depending on the type
|
||||||
config?: TConfig;
|
config?: TConfig;
|
||||||
|
|
||||||
// Standard options avaliable for all elements
|
// Standard options available for all elements
|
||||||
anchor?: Anchor; // defaults top, left, width and height
|
anchor?: Anchor; // defaults top, left, width and height
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
background?: BackgroundConfig;
|
background?: BackgroundConfig;
|
||||||
@@ -50,7 +51,7 @@ export interface CanvasElementItem<TConfig = any, TData = any> extends RegistryI
|
|||||||
/** Component used to draw */
|
/** Component used to draw */
|
||||||
display: ComponentType<CanvasElementProps<TConfig, TData>>;
|
display: ComponentType<CanvasElementProps<TConfig, TData>>;
|
||||||
|
|
||||||
getNewOptions: (options?: CanvasElementOptions) => Omit<CanvasElementOptions<TConfig>, 'type'>;
|
getNewOptions: (options?: CanvasElementOptions) => Omit<CanvasElementOptions<TConfig>, 'type' | 'name'>;
|
||||||
|
|
||||||
/** Build the configuraiton UI */
|
/** Build the configuraiton UI */
|
||||||
registerOptionsUI?: PanelOptionsSupplier<CanvasElementOptions<TConfig>>;
|
registerOptionsUI?: PanelOptionsSupplier<CanvasElementOptions<TConfig>>;
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { iconItem } from './elements/icon';
|
|||||||
import { textBoxItem } from './elements/textBox';
|
import { textBoxItem } from './elements/textBox';
|
||||||
|
|
||||||
export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = {
|
export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = {
|
||||||
type: iconItem.id,
|
|
||||||
...iconItem.getNewOptions(),
|
...iconItem.getNewOptions(),
|
||||||
|
type: iconItem.id,
|
||||||
|
name: `Group ${Date.now()}.${Math.floor(Math.random() * 100)}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canvasElementRegistry = new Registry<CanvasElementItem>(() => [
|
export const canvasElementRegistry = new Registry<CanvasElementItem>(() => [
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import {
|
|||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
||||||
import { GroupState } from './group';
|
import { GroupState } from './group';
|
||||||
|
import { LayerElement } from 'app/core/components/Layers/types';
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
export class ElementState {
|
export class ElementState implements LayerElement {
|
||||||
readonly UID = counter++;
|
readonly UID = counter++;
|
||||||
|
|
||||||
revId = 0;
|
revId = 0;
|
||||||
@@ -36,12 +37,20 @@ export class ElementState {
|
|||||||
|
|
||||||
constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: GroupState) {
|
constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: GroupState) {
|
||||||
if (!options) {
|
if (!options) {
|
||||||
this.options = { type: item.id };
|
this.options = { type: item.id, name: `Element ${this.UID}` };
|
||||||
}
|
}
|
||||||
this.anchor = options.anchor ?? {};
|
this.anchor = options.anchor ?? {};
|
||||||
this.placement = options.placement ?? {};
|
this.placement = options.placement ?? {};
|
||||||
options.anchor = this.anchor;
|
options.anchor = this.anchor;
|
||||||
options.placement = this.placement;
|
options.placement = this.placement;
|
||||||
|
|
||||||
|
if (!options.name) {
|
||||||
|
options.name = `Element ${this.UID}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.options.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePlacement() {
|
validatePlacement() {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export class GroupState extends ElementState {
|
|||||||
|
|
||||||
// ??? or should this be on the element directly?
|
// ??? or should this be on the element directly?
|
||||||
// are actions scoped to layers?
|
// are actions scoped to layers?
|
||||||
doAction = (action: LayerActionID, element: ElementState) => {
|
doAction = (action: LayerActionID, element: ElementState, updateName = true) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case LayerActionID.Delete:
|
case LayerActionID.Delete:
|
||||||
this.elements = this.elements.filter((e) => e !== element);
|
this.elements = this.elements.filter((e) => e !== element);
|
||||||
@@ -128,6 +128,9 @@ export class GroupState extends ElementState {
|
|||||||
const copy = new ElementState(element.item, opts, this);
|
const copy = new ElementState(element.item, opts, this);
|
||||||
copy.updateSize(element.width, element.height);
|
copy.updateSize(element.width, element.height);
|
||||||
copy.updateData(this.scene.context);
|
copy.updateData(this.scene.context);
|
||||||
|
if (updateName) {
|
||||||
|
copy.options.name = `Element ${copy.UID} (duplicate)`;
|
||||||
|
}
|
||||||
this.elements.push(copy);
|
this.elements.push(copy);
|
||||||
this.scene.save();
|
this.scene.save();
|
||||||
this.reinitializeMoveable();
|
this.reinitializeMoveable();
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export class Scene {
|
|||||||
const newLayer = new GroupState(
|
const newLayer = new GroupState(
|
||||||
{
|
{
|
||||||
type: 'group',
|
type: 'group',
|
||||||
|
name: `Group ${Date.now()}.${Math.floor(Math.random() * 100)}`,
|
||||||
elements: [],
|
elements: [],
|
||||||
},
|
},
|
||||||
this,
|
this,
|
||||||
@@ -110,7 +111,7 @@ export class Scene {
|
|||||||
);
|
);
|
||||||
|
|
||||||
currentSelectedElements.forEach((element: ElementState) => {
|
currentSelectedElements.forEach((element: ElementState) => {
|
||||||
newLayer.doAction(LayerActionID.Duplicate, element);
|
newLayer.doAction(LayerActionID.Duplicate, element, false);
|
||||||
currentLayer.doAction(LayerActionID.Delete, element);
|
currentLayer.doAction(LayerActionID.Delete, element);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { Button, HorizontalGroup } from '@grafana/ui';
|
||||||
import { Button, HorizontalGroup, Icon, IconButton, stylesFactory, ValuePicker } from '@grafana/ui';
|
import { AppEvents, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||||
import { AppEvents, GrafanaTheme, SelectableValue, StandardEditorProps } from '@grafana/data';
|
import { DropResult } from 'react-beautiful-dnd';
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
|
||||||
|
|
||||||
import { PanelOptions } from '../models.gen';
|
import { PanelOptions } from '../models.gen';
|
||||||
import { LayerActionID } from '../types';
|
import { LayerActionID } from '../types';
|
||||||
@@ -15,12 +13,12 @@ import { GroupState } from 'app/features/canvas/runtime/group';
|
|||||||
import { LayerEditorProps } from './layerEditor';
|
import { LayerEditorProps } from './layerEditor';
|
||||||
import { SelectionParams } from 'app/features/canvas/runtime/scene';
|
import { SelectionParams } from 'app/features/canvas/runtime/scene';
|
||||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||||
|
import { LayerDragDropList } from 'app/core/components/Layers/LayerDragDropList';
|
||||||
|
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
||||||
|
|
||||||
type Props = StandardEditorProps<any, LayerEditorProps, PanelOptions>;
|
type Props = StandardEditorProps<any, LayerEditorProps, PanelOptions>;
|
||||||
|
|
||||||
export class LayerElementListEditor extends PureComponent<Props> {
|
export class LayerElementListEditor extends PureComponent<Props> {
|
||||||
style = getLayerDragStyles(config.theme);
|
|
||||||
|
|
||||||
getScene = () => {
|
getScene = () => {
|
||||||
const { settings } = this.props.item;
|
const { settings } = this.props.item;
|
||||||
if (!settings?.layer) {
|
if (!settings?.layer) {
|
||||||
@@ -86,10 +84,6 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
layer.scene.clearCurrentSelection();
|
layer.scene.clearCurrentSelection();
|
||||||
};
|
};
|
||||||
|
|
||||||
getRowStyle = (sel: boolean) => {
|
|
||||||
return sel ? `${this.style.row} ${this.style.sel}` : this.style.row;
|
|
||||||
};
|
|
||||||
|
|
||||||
onDragEnd = (result: DropResult) => {
|
onDragEnd = (result: DropResult) => {
|
||||||
if (!result.destination) {
|
if (!result.destination) {
|
||||||
return;
|
return;
|
||||||
@@ -133,7 +127,7 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
const { layer } = settings;
|
const { layer } = settings;
|
||||||
|
|
||||||
layer.elements.forEach((element: ElementState) => {
|
layer.elements.forEach((element: ElementState) => {
|
||||||
layer.parent?.doAction(LayerActionID.Duplicate, element);
|
layer.parent?.doAction(LayerActionID.Duplicate, element, false);
|
||||||
});
|
});
|
||||||
this.deleteGroup();
|
this.deleteGroup();
|
||||||
};
|
};
|
||||||
@@ -201,8 +195,27 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
return <div>Missing layer?</div>;
|
return <div>Missing layer?</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = this.style;
|
const onDelete = (element: ElementState) => {
|
||||||
const selection: number[] = settings.selected ? settings.selected.map((v) => v.UID) : [];
|
layer.doAction(LayerActionID.Delete, element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDuplicate = (element: ElementState) => {
|
||||||
|
layer.doAction(LayerActionID.Duplicate, element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLayerInfo = (element: ElementState) => {
|
||||||
|
return element.options.type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNameChange = (element: ElementState, name: string) => {
|
||||||
|
element.onChange({ ...element.options, name });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGroup = (element: ElementState) => {
|
||||||
|
return element instanceof GroupState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selection: string[] = settings.selected ? settings.selected.map((v) => v.getName()) : [];
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!layer.isRoot() && (
|
{!layer.isRoot() && (
|
||||||
@@ -221,78 +234,24 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<DragDropContext onDragEnd={this.onDragEnd}>
|
<LayerDragDropList
|
||||||
<Droppable droppableId="droppable">
|
onDragEnd={this.onDragEnd}
|
||||||
{(provided, snapshot) => (
|
onSelect={this.onSelect}
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
onDelete={onDelete}
|
||||||
{(() => {
|
onDuplicate={onDuplicate}
|
||||||
// reverse order
|
getLayerInfo={getLayerInfo}
|
||||||
const rows: any = [];
|
onNameChange={onNameChange}
|
||||||
for (let i = layer.elements.length - 1; i >= 0; i--) {
|
isGroup={isGroup}
|
||||||
const element = layer.elements[i];
|
layers={layer.elements}
|
||||||
rows.push(
|
selection={selection}
|
||||||
<Draggable key={element.UID} draggableId={`${element.UID}`} index={rows.length}>
|
|
||||||
{(provided, snapshot) => (
|
|
||||||
<div
|
|
||||||
className={this.getRowStyle(selection.includes(element.UID))}
|
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.draggableProps}
|
|
||||||
{...provided.dragHandleProps}
|
|
||||||
onMouseDown={() => this.onSelect(element)}
|
|
||||||
>
|
|
||||||
<span className={styles.typeWrapper}>{element.item.name}</span>
|
|
||||||
<div className={styles.textWrapper}>
|
|
||||||
{element.UID} ({i})
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{element.item.id !== 'group' && (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
name="copy"
|
|
||||||
title={'Duplicate'}
|
|
||||||
className={styles.actionIcon}
|
|
||||||
onClick={() => layer.doAction(LayerActionID.Duplicate, element)}
|
|
||||||
surface="header"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
|
||||||
name="trash-alt"
|
|
||||||
title={'Remove'}
|
|
||||||
className={cx(styles.actionIcon, styles.dragIcon)}
|
|
||||||
onClick={() => layer.doAction(LayerActionID.Delete, element)}
|
|
||||||
surface="header"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
title="Drag and drop to reorder"
|
|
||||||
name="draggabledots"
|
|
||||||
size="lg"
|
|
||||||
className={styles.dragIcon}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{provided.placeholder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<HorizontalGroup>
|
<HorizontalGroup>
|
||||||
<ValuePicker
|
<AddLayerButton
|
||||||
icon="plus"
|
|
||||||
label="Add item"
|
|
||||||
variant="secondary"
|
|
||||||
options={canvasElementRegistry.selectOptions().options}
|
|
||||||
onChange={this.onAddItem}
|
onChange={this.onAddItem}
|
||||||
isFullWidth={false}
|
options={canvasElementRegistry.selectOptions().options}
|
||||||
|
label={'Add item'}
|
||||||
/>
|
/>
|
||||||
{selection.length > 0 && (
|
{selection.length > 0 && (
|
||||||
<Button size="sm" variant="secondary" onClick={this.onClearSelection}>
|
<Button size="sm" variant="secondary" onClick={this.onClearSelection}>
|
||||||
@@ -309,51 +268,3 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getLayerDragStyles = stylesFactory((theme: GrafanaTheme) => ({
|
|
||||||
wrapper: css`
|
|
||||||
margin-bottom: ${theme.spacing.md};
|
|
||||||
`,
|
|
||||||
row: css`
|
|
||||||
padding: ${theme.spacing.xs} ${theme.spacing.sm};
|
|
||||||
border-radius: ${theme.border.radius.sm};
|
|
||||||
background: ${theme.colors.bg2};
|
|
||||||
min-height: ${theme.spacing.formInputHeight}px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
border: 1px solid ${theme.colors.formInputBorder};
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid ${theme.colors.formInputBorderHover};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
sel: css`
|
|
||||||
border: 1px solid ${theme.colors.formInputBorderActive};
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid ${theme.colors.formInputBorderActive};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
dragIcon: css`
|
|
||||||
cursor: drag;
|
|
||||||
`,
|
|
||||||
actionIcon: css`
|
|
||||||
color: ${theme.colors.textWeak};
|
|
||||||
&:hover {
|
|
||||||
color: ${theme.colors.text};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
typeWrapper: css`
|
|
||||||
color: ${theme.colors.textBlue};
|
|
||||||
margin-right: 5px;
|
|
||||||
`,
|
|
||||||
textWrapper: css`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-right: ${theme.spacing.sm};
|
|
||||||
`,
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<
|
|||||||
currentOptions = {
|
currentOptions = {
|
||||||
...layer.getNewOptions(options),
|
...layer.getNewOptions(options),
|
||||||
type: layer.id,
|
type: layer.id,
|
||||||
|
name: `Element ${Date.now()}.${Math.floor(Math.random() * 100)}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const ctx = { ...context, options: currentOptions };
|
const ctx = { ...context, options: currentOptions };
|
||||||
|
|||||||
@@ -453,6 +453,8 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
layer,
|
layer,
|
||||||
handler,
|
handler,
|
||||||
|
|
||||||
|
getName: () => UID,
|
||||||
|
|
||||||
// Used by the editors
|
// Used by the editors
|
||||||
onChange: (cfg: MapLayerOptions) => {
|
onChange: (cfg: MapLayerOptions) => {
|
||||||
this.updateLayer(UID, cfg);
|
this.updateLayer(UID, cfg);
|
||||||
|
|||||||
81
public/app/plugins/panel/geomap/editor/LayersEditor.tsx
Normal file
81
public/app/plugins/panel/geomap/editor/LayersEditor.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Container } from '@grafana/ui';
|
||||||
|
import { StandardEditorProps } from '@grafana/data';
|
||||||
|
import { DropResult } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { GeomapPanelOptions, MapLayerState } from '../types';
|
||||||
|
import { GeomapInstanceState } from '../GeomapPanel';
|
||||||
|
import { geomapLayerRegistry } from '../layers/registry';
|
||||||
|
import { dataLayerFilter } from './layerEditor';
|
||||||
|
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
||||||
|
import { LayerDragDropList } from 'app/core/components/Layers/LayerDragDropList';
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = (element: MapLayerState<any>) => {
|
||||||
|
actions.selectLayer(element.options.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (element: MapLayerState<any>) => {
|
||||||
|
actions.deleteLayer(element.options.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLayerInfo = (element: MapLayerState<any>) => {
|
||||||
|
return element.options.type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNameChange = (element: MapLayerState<any>, name: string) => {
|
||||||
|
element.onChange({ ...element.options, name });
|
||||||
|
};
|
||||||
|
|
||||||
|
const selection = selected ? [layers[selected]?.getName()] : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container>
|
||||||
|
<AddLayerButton
|
||||||
|
onChange={(v) => actions.addlayer(v.value!)}
|
||||||
|
options={geomapLayerRegistry.selectOptions(undefined, dataLayerFilter).options}
|
||||||
|
label={'Add layer'}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<LayerDragDropList
|
||||||
|
layers={layers}
|
||||||
|
getLayerInfo={getLayerInfo}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onDelete={onDelete}
|
||||||
|
selection={selection}
|
||||||
|
excludeBaseLayer
|
||||||
|
onNameChange={onNameChange}
|
||||||
|
verifyLayerNameUniqueness={actions.canRename}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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];
|
|
||||||
const uid = element.options.name;
|
|
||||||
rows.push(
|
|
||||||
<Draggable key={uid} draggableId={uid} index={rows.length}>
|
|
||||||
{(provided, snapshot) => (
|
|
||||||
<div
|
|
||||||
className={getRowStyle(i === selected)}
|
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.draggableProps}
|
|
||||||
{...provided.dragHandleProps}
|
|
||||||
onMouseDown={() => actions!.selectLayer(uid)}
|
|
||||||
>
|
|
||||||
<LayerHeader
|
|
||||||
layer={element.options}
|
|
||||||
canRename={actions.canRename}
|
|
||||||
onChange={element.onChange}
|
|
||||||
/>
|
|
||||||
<div className={style.textWrapper}> {element.options.type}</div>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
name="trash-alt"
|
|
||||||
title={'remove'}
|
|
||||||
className={cx(style.actionIcon, style.dragIcon)}
|
|
||||||
onClick={() => actions.deleteLayer(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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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/LayersEditor';
|
import { LayersEditor } from './editor/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)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MapLayerHandler, MapLayerOptions, SelectableValue } from '@grafana/data';
|
import { MapLayerHandler, MapLayerOptions, SelectableValue } from '@grafana/data';
|
||||||
|
import { LayerElement } from 'app/core/components/Layers/types';
|
||||||
import BaseLayer from 'ol/layer/Base';
|
import BaseLayer from 'ol/layer/Base';
|
||||||
import { Units } from 'ol/proj/Units';
|
import { Units } from 'ol/proj/Units';
|
||||||
import { StyleConfig } from './style/types';
|
import { StyleConfig } from './style/types';
|
||||||
@@ -70,7 +71,7 @@ export interface GazetteerPathEditorConfigSettings {
|
|||||||
//-------------------
|
//-------------------
|
||||||
// Runtime model
|
// Runtime model
|
||||||
//-------------------
|
//-------------------
|
||||||
export interface MapLayerState<TConfig = any> {
|
export interface MapLayerState<TConfig = any> extends LayerElement {
|
||||||
options: MapLayerOptions<TConfig>;
|
options: MapLayerOptions<TConfig>;
|
||||||
handler: MapLayerHandler;
|
handler: MapLayerHandler;
|
||||||
layer: BaseLayer; // the openlayers instance
|
layer: BaseLayer; // the openlayers instance
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { IconConfig } from 'app/features/canvas/elements/icon';
|
|||||||
import { ResourceDimensionMode } from 'app/features/dimensions';
|
import { ResourceDimensionMode } from 'app/features/dimensions';
|
||||||
|
|
||||||
export interface PanelOptions {
|
export interface PanelOptions {
|
||||||
root: Omit<CanvasElementOptions<IconConfig>, 'type'>; // type is forced
|
root: Omit<CanvasElementOptions<IconConfig>, 'type' | 'name'>; // type is forced
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPanelOptions: PanelOptions = {
|
export const defaultPanelOptions: PanelOptions = {
|
||||||
|
|||||||
Reference in New Issue
Block a user