mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Add button element + basic onClick support (#43252)
This commit is contained in:
@@ -5,19 +5,26 @@ import { DimensionContext } from 'app/features/dimensions/context';
|
|||||||
import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor';
|
import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor';
|
||||||
import { TextDimensionConfig } from 'app/features/dimensions/types';
|
import { TextDimensionConfig } from 'app/features/dimensions/types';
|
||||||
import { CanvasElementItem, CanvasElementProps } from '../element';
|
import { CanvasElementItem, CanvasElementProps } from '../element';
|
||||||
|
import { APIEditor, APIEditorConfig, callApi } from 'app/plugins/panel/canvas/editor/APIEditor';
|
||||||
|
|
||||||
interface ButtonData {
|
interface ButtonData {
|
||||||
text?: string;
|
text?: string;
|
||||||
|
api?: APIEditorConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ButtonConfig {
|
interface ButtonConfig {
|
||||||
text?: TextDimensionConfig;
|
text?: TextDimensionConfig;
|
||||||
|
api?: APIEditorConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ButtonDisplay extends PureComponent<CanvasElementProps<ButtonConfig, ButtonData>> {
|
class ButtonDisplay extends PureComponent<CanvasElementProps<ButtonConfig, ButtonData>> {
|
||||||
render() {
|
render() {
|
||||||
const { data } = this.props;
|
const { data } = this.props;
|
||||||
const onClick = () => console.log('button being clicked :)');
|
const onClick = () => {
|
||||||
|
if (data?.api) {
|
||||||
|
callApi(data.api);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return <Button onClick={onClick}>{data?.text}</Button>;
|
return <Button onClick={onClick}>{data?.text}</Button>;
|
||||||
}
|
}
|
||||||
@@ -43,6 +50,7 @@ export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
|
|||||||
prepareData: (ctx: DimensionContext, cfg: ButtonConfig) => {
|
prepareData: (ctx: DimensionContext, cfg: ButtonConfig) => {
|
||||||
const data: ButtonData = {
|
const data: ButtonData = {
|
||||||
text: cfg?.text ? ctx.getText(cfg.text).value() : '',
|
text: cfg?.text ? ctx.getText(cfg.text).value() : '',
|
||||||
|
api: cfg?.api ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -51,12 +59,20 @@ export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
|
|||||||
// Heatmap overlay options
|
// Heatmap overlay options
|
||||||
registerOptionsUI: (builder) => {
|
registerOptionsUI: (builder) => {
|
||||||
const category = ['Button'];
|
const category = ['Button'];
|
||||||
builder.addCustomEditor({
|
builder
|
||||||
|
.addCustomEditor({
|
||||||
category,
|
category,
|
||||||
id: 'textSelector',
|
id: 'textSelector',
|
||||||
path: 'config.text',
|
path: 'config.text',
|
||||||
name: 'Text',
|
name: 'Text',
|
||||||
editor: TextDimensionEditor,
|
editor: TextDimensionEditor,
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
category,
|
||||||
|
id: 'apiSelector',
|
||||||
|
path: 'config.api',
|
||||||
|
name: 'API',
|
||||||
|
editor: APIEditor,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import { css } from '@emotion/css';
|
|||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
import { LineConfig } from '../types';
|
import { LineConfig } from '../types';
|
||||||
import { DimensionContext } from 'app/features/dimensions/context';
|
import { DimensionContext } from 'app/features/dimensions/context';
|
||||||
|
import { APIEditor, APIEditorConfig, callApi } from 'app/plugins/panel/canvas/editor/APIEditor';
|
||||||
|
|
||||||
export interface IconConfig {
|
export interface IconConfig {
|
||||||
path?: ResourceDimensionConfig;
|
path?: ResourceDimensionConfig;
|
||||||
fill?: ColorDimensionConfig;
|
fill?: ColorDimensionConfig;
|
||||||
stroke?: LineConfig;
|
stroke?: LineConfig;
|
||||||
|
api?: APIEditorConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IconData {
|
interface IconData {
|
||||||
@@ -25,6 +27,7 @@ interface IconData {
|
|||||||
fill: string;
|
fill: string;
|
||||||
strokeColor?: string;
|
strokeColor?: string;
|
||||||
stroke?: number;
|
stroke?: number;
|
||||||
|
api?: APIEditorConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a stoke is defined, we want the path to be in page units
|
// When a stoke is defined, we want the path to be in page units
|
||||||
@@ -40,6 +43,12 @@ export function IconDisplay(props: CanvasElementProps) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if (data?.api) {
|
||||||
|
callApi(data.api);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const svgStyle: CSSProperties = {
|
const svgStyle: CSSProperties = {
|
||||||
fill: data?.fill,
|
fill: data?.fill,
|
||||||
stroke: data?.strokeColor,
|
stroke: data?.strokeColor,
|
||||||
@@ -48,6 +57,7 @@ export function IconDisplay(props: CanvasElementProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SVG
|
<SVG
|
||||||
|
onClick={onClick}
|
||||||
src={data.path}
|
src={data.path}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
@@ -92,6 +102,7 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
|
|||||||
const data: IconData = {
|
const data: IconData = {
|
||||||
path,
|
path,
|
||||||
fill: cfg.fill ? ctx.getColor(cfg.fill).value() : '#CCC',
|
fill: cfg.fill ? ctx.getColor(cfg.fill).value() : '#CCC',
|
||||||
|
api: cfg?.api ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (cfg.stroke?.width && cfg.stroke.color) {
|
if (cfg.stroke?.width && cfg.stroke.color) {
|
||||||
@@ -151,6 +162,13 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
|
|||||||
fixed: 'grey',
|
fixed: 'grey',
|
||||||
},
|
},
|
||||||
showIf: (cfg) => Boolean(cfg?.config?.stroke?.width),
|
showIf: (cfg) => Boolean(cfg?.config?.stroke?.width),
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
category,
|
||||||
|
id: 'apiSelector',
|
||||||
|
path: 'config.api',
|
||||||
|
name: 'API',
|
||||||
|
editor: APIEditor,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Registry } from '@grafana/data';
|
import { Registry } from '@grafana/data';
|
||||||
import { CanvasElementItem, CanvasElementOptions } from './element';
|
import { CanvasElementItem, CanvasElementOptions } from './element';
|
||||||
|
import { buttonItem } from './elements/button';
|
||||||
import { droneFrontItem } from './elements/droneFront';
|
import { droneFrontItem } from './elements/droneFront';
|
||||||
import { droneSideItem } from './elements/droneSide';
|
import { droneSideItem } from './elements/droneSide';
|
||||||
import { droneTopItem } from './elements/droneTop';
|
import { droneTopItem } from './elements/droneTop';
|
||||||
@@ -15,6 +16,7 @@ export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = {
|
|||||||
export const canvasElementRegistry = new Registry<CanvasElementItem>(() => [
|
export const canvasElementRegistry = new Registry<CanvasElementItem>(() => [
|
||||||
iconItem, // default for now
|
iconItem, // default for now
|
||||||
textBoxItem,
|
textBoxItem,
|
||||||
|
buttonItem,
|
||||||
droneTopItem,
|
droneTopItem,
|
||||||
droneFrontItem,
|
droneFrontItem,
|
||||||
droneSideItem,
|
droneSideItem,
|
||||||
|
|||||||
114
public/app/plugins/panel/canvas/editor/APIEditor.tsx
Normal file
114
public/app/plugins/panel/canvas/editor/APIEditor.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import { Button, InlineField, InlineFieldRow, JSONFormatter, StringValueEditor } from '@grafana/ui';
|
||||||
|
import { AppEvents, StandardEditorProps, StandardEditorsRegistryItem, StringFieldConfigSettings } from '@grafana/data';
|
||||||
|
import { config, getBackendSrv } from '@grafana/runtime';
|
||||||
|
import { appEvents } from 'app/core/core';
|
||||||
|
|
||||||
|
export interface APIEditorConfig {
|
||||||
|
endpoint: string;
|
||||||
|
data?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dummyStringSettings: StandardEditorsRegistryItem<string, StringFieldConfigSettings> = {
|
||||||
|
settings: {},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
export const callApi = (api: APIEditorConfig, isTest = false) => {
|
||||||
|
if (api) {
|
||||||
|
getBackendSrv()
|
||||||
|
.fetch({
|
||||||
|
url: api.endpoint!,
|
||||||
|
method: 'POST',
|
||||||
|
data: api.data ?? {},
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
error: (error: any) => {
|
||||||
|
if (isTest) {
|
||||||
|
appEvents.emit(AppEvents.alertError, ['Error has occurred: ', JSON.stringify(error)]);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
if (isTest) {
|
||||||
|
appEvents.emit(AppEvents.alertSuccess, ['Test successful']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const APIEditor: FC<StandardEditorProps<APIEditorConfig, any, any>> = (props) => {
|
||||||
|
const { value, context, onChange } = props;
|
||||||
|
const labelWidth = 9;
|
||||||
|
|
||||||
|
const onEndpointChange = useCallback(
|
||||||
|
(endpoint) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
endpoint,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange, value]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDataChange = useCallback(
|
||||||
|
(data) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange, value]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderJSON = (data: string) => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
return <JSONFormatter json={json} />;
|
||||||
|
} catch (error) {
|
||||||
|
return `Invalid JSON provided: ${error.message}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTestAPIButton = (api: APIEditorConfig) => {
|
||||||
|
if (api && api.endpoint) {
|
||||||
|
return (
|
||||||
|
<Button onClick={() => callApi(api, true)} title={'Test API'}>
|
||||||
|
Test API
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
return config.disableSanitizeHtml ? (
|
||||||
|
<>
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label={'Endpoint'} labelWidth={labelWidth} grow={true}>
|
||||||
|
<StringValueEditor
|
||||||
|
context={context}
|
||||||
|
value={value?.endpoint}
|
||||||
|
onChange={onEndpointChange}
|
||||||
|
item={dummyStringSettings}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label={'Data'} labelWidth={labelWidth} grow={true}>
|
||||||
|
<StringValueEditor
|
||||||
|
context={context}
|
||||||
|
value={value?.data ?? '{}'}
|
||||||
|
onChange={onDataChange}
|
||||||
|
item={dummyStringSettings}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
{renderTestAPIButton(value)}
|
||||||
|
<br />
|
||||||
|
{renderJSON(value?.data ?? '{}')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Must enable disableSanitizeHtml feature flag to access</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user