diff --git a/public/app/features/canvas/elements/button.tsx b/public/app/features/canvas/elements/button.tsx index 6e0a6559e49..482a432b2b8 100644 --- a/public/app/features/canvas/elements/button.tsx +++ b/public/app/features/canvas/elements/button.tsx @@ -5,19 +5,26 @@ import { DimensionContext } from 'app/features/dimensions/context'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; import { TextDimensionConfig } from 'app/features/dimensions/types'; import { CanvasElementItem, CanvasElementProps } from '../element'; +import { APIEditor, APIEditorConfig, callApi } from 'app/plugins/panel/canvas/editor/APIEditor'; interface ButtonData { text?: string; + api?: APIEditorConfig; } interface ButtonConfig { text?: TextDimensionConfig; + api?: APIEditorConfig; } class ButtonDisplay extends PureComponent> { render() { const { data } = this.props; - const onClick = () => console.log('button being clicked :)'); + const onClick = () => { + if (data?.api) { + callApi(data.api); + } + }; return ; } @@ -43,6 +50,7 @@ export const buttonItem: CanvasElementItem = { prepareData: (ctx: DimensionContext, cfg: ButtonConfig) => { const data: ButtonData = { text: cfg?.text ? ctx.getText(cfg.text).value() : '', + api: cfg?.api ?? undefined, }; return data; @@ -51,12 +59,20 @@ export const buttonItem: CanvasElementItem = { // Heatmap overlay options registerOptionsUI: (builder) => { const category = ['Button']; - builder.addCustomEditor({ - category, - id: 'textSelector', - path: 'config.text', - name: 'Text', - editor: TextDimensionEditor, - }); + builder + .addCustomEditor({ + category, + id: 'textSelector', + path: 'config.text', + name: 'Text', + editor: TextDimensionEditor, + }) + .addCustomEditor({ + category, + id: 'apiSelector', + path: 'config.api', + name: 'API', + editor: APIEditor, + }); }, }; diff --git a/public/app/features/canvas/elements/icon.tsx b/public/app/features/canvas/elements/icon.tsx index 1c28ad37afc..53652474c1e 100644 --- a/public/app/features/canvas/elements/icon.tsx +++ b/public/app/features/canvas/elements/icon.tsx @@ -13,11 +13,13 @@ import { css } from '@emotion/css'; import { isString } from 'lodash'; import { LineConfig } from '../types'; import { DimensionContext } from 'app/features/dimensions/context'; +import { APIEditor, APIEditorConfig, callApi } from 'app/plugins/panel/canvas/editor/APIEditor'; export interface IconConfig { path?: ResourceDimensionConfig; fill?: ColorDimensionConfig; stroke?: LineConfig; + api?: APIEditorConfig; } interface IconData { @@ -25,6 +27,7 @@ interface IconData { fill: string; strokeColor?: string; stroke?: number; + api?: APIEditorConfig; } // 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; } + const onClick = () => { + if (data?.api) { + callApi(data.api); + } + }; + const svgStyle: CSSProperties = { fill: data?.fill, stroke: data?.strokeColor, @@ -48,6 +57,7 @@ export function IconDisplay(props: CanvasElementProps) { return ( = { const data: IconData = { path, fill: cfg.fill ? ctx.getColor(cfg.fill).value() : '#CCC', + api: cfg?.api ?? undefined, }; if (cfg.stroke?.width && cfg.stroke.color) { @@ -151,6 +162,13 @@ export const iconItem: CanvasElementItem = { fixed: 'grey', }, showIf: (cfg) => Boolean(cfg?.config?.stroke?.width), + }) + .addCustomEditor({ + category, + id: 'apiSelector', + path: 'config.api', + name: 'API', + editor: APIEditor, }); }, }; diff --git a/public/app/features/canvas/registry.ts b/public/app/features/canvas/registry.ts index 90084152048..642ef0668e4 100644 --- a/public/app/features/canvas/registry.ts +++ b/public/app/features/canvas/registry.ts @@ -1,5 +1,6 @@ import { Registry } from '@grafana/data'; import { CanvasElementItem, CanvasElementOptions } from './element'; +import { buttonItem } from './elements/button'; import { droneFrontItem } from './elements/droneFront'; import { droneSideItem } from './elements/droneSide'; import { droneTopItem } from './elements/droneTop'; @@ -15,6 +16,7 @@ export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = { export const canvasElementRegistry = new Registry(() => [ iconItem, // default for now textBoxItem, + buttonItem, droneTopItem, droneFrontItem, droneSideItem, diff --git a/public/app/plugins/panel/canvas/editor/APIEditor.tsx b/public/app/plugins/panel/canvas/editor/APIEditor.tsx new file mode 100644 index 00000000000..85ddf302ee2 --- /dev/null +++ b/public/app/plugins/panel/canvas/editor/APIEditor.tsx @@ -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 = { + 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> = (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 ; + } catch (error) { + return `Invalid JSON provided: ${error.message}`; + } + }; + + const renderTestAPIButton = (api: APIEditorConfig) => { + if (api && api.endpoint) { + return ( + + ); + } + + return; + }; + + return config.disableSanitizeHtml ? ( + <> + + + + + + + + + + + {renderTestAPIButton(value)} +
+ {renderJSON(value?.data ?? '{}')} + + ) : ( + <>Must enable disableSanitizeHtml feature flag to access + ); +};