Canvas: Button API Editor visual feedback on response (#76499)

This commit is contained in:
Adela Almasan 2023-10-13 00:11:08 -05:00 committed by GitHub
parent 18128c2666
commit d5945bc26e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 71 additions and 55 deletions

View File

@ -92,4 +92,4 @@ export interface CanvasElementItem<TConfig = any, TData = any> extends RegistryI
export const defaultBgColor = '#D9D9D9'; export const defaultBgColor = '#D9D9D9';
export const defaultTextColor = '#000000'; export const defaultTextColor = '#000000';
export const defaultLightTextColor = '#F0F4FD'; export const defaultLightTextColor = '#F0F4FD';
export const defaultThemeTextColor = config.theme2.colors.background.primary; export const defaultThemeTextColor = config.theme2.colors.text.primary;

View File

@ -1,11 +1,10 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { PureComponent } from 'react'; import React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { PluginState } from '@grafana/data/src'; import { PluginState } from '@grafana/data/src';
import { TextDimensionMode } from '@grafana/schema'; import { TextDimensionMode } from '@grafana/schema';
import { Button, stylesFactory } from '@grafana/ui'; import { Button, Spinner, useStyles2 } from '@grafana/ui';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions/context'; import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors'; import { ColorDimensionEditor } from 'app/features/dimensions/editors';
import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor';
@ -40,26 +39,39 @@ export const defaultStyleConfig: ButtonStyleConfig = {
variant: 'primary', variant: 'primary',
}; };
class ButtonDisplay extends PureComponent<CanvasElementProps<ButtonConfig, ButtonData>> { const ButtonDisplay = ({ data }: CanvasElementProps<ButtonConfig, ButtonData>) => {
render() { const styles = useStyles2(getStyles, data);
const { data } = this.props;
const styles = getStyles(config.theme2, data);
const onClick = () => { const [isLoading, setIsLoading] = React.useState(false);
if (data?.api && data?.api?.endpoint) {
callApi(data.api);
}
};
return ( const updateLoadingStateCallback = (loading: boolean) => {
<Button type="submit" variant={data?.style?.variant} onClick={onClick} className={styles.button}> setIsLoading(loading);
};
const onClick = () => {
if (data?.api && data?.api?.endpoint) {
setIsLoading(true);
callApi(data.api, updateLoadingStateCallback);
}
};
return (
<Button
type="submit"
variant={data?.style?.variant}
onClick={onClick}
className={styles.button}
disabled={!data?.api?.endpoint}
>
<span>
{isLoading && <Spinner inline={true} className={styles.buttonSpinner} />}
{data?.text} {data?.text}
</Button> </span>
); </Button>
} );
} };
const getStyles = stylesFactory((theme: GrafanaTheme2, data: ButtonData | undefined) => ({ const getStyles = (theme: GrafanaTheme2, data: ButtonData | undefined) => ({
button: css({ button: css({
height: '100%', height: '100%',
width: '100%', width: '100%',
@ -67,12 +79,16 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, data: ButtonData | undefi
'> span': { '> span': {
display: 'inline-grid', display: 'inline-grid',
gridAutoFlow: 'column',
textAlign: data?.align, textAlign: data?.align,
fontSize: `${data?.size}px`, fontSize: `${data?.size}px`,
color: data?.color, color: data?.color,
}, },
}), }),
})); buttonSpinner: css({
marginRight: theme.spacing(0.5),
}),
});
export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = { export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
id: 'button', id: 'button',
@ -137,7 +153,7 @@ export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
const data: ButtonData = { const data: ButtonData = {
text: cfg?.text ? ctx.getText(cfg.text).value() : '', text: cfg?.text ? ctx.getText(cfg.text).value() : '',
align: cfg.align ?? Align.Center, align: cfg.align ?? Align.Center,
size: cfg.size, size: cfg.size ?? 14,
api: getCfgApi(), api: getCfgApi(),
style: cfg?.style ?? defaultStyleConfig, style: cfg?.style ?? defaultStyleConfig,
}; };

View File

@ -132,7 +132,7 @@ export function APIEditor({ value, context, onChange }: Props) {
const renderTestAPIButton = (api: APIEditorConfig) => { const renderTestAPIButton = (api: APIEditorConfig) => {
if (api && api.endpoint) { if (api && api.endpoint) {
return ( return (
<Button onClick={() => callApi(api, true)} title="Test API"> <Button onClick={() => callApi(api)} title="Test API">
Test API Test API
</Button> </Button>
); );
@ -158,36 +158,36 @@ export function APIEditor({ value, context, onChange }: Props) {
<RadioButtonGroup value={value?.method} options={httpMethodOptions} onChange={onMethodChange} fullWidth /> <RadioButtonGroup value={value?.method} options={httpMethodOptions} onChange={onMethodChange} fullWidth />
</InlineField> </InlineField>
</InlineFieldRow> </InlineFieldRow>
{value?.method === HttpRequestMethod.POST && (
<InlineFieldRow>
<InlineField label="Content-Type" labelWidth={LABEL_WIDTH} grow={true}>
<Select
options={contentTypeOptions}
allowCustomValue={true}
formatCreateLabel={formatCreateLabel}
value={value?.contentType}
onChange={onContentTypeChange}
/>
</InlineField>
</InlineFieldRow>
)}
<br />
<Field label="Query parameters"> <Field label="Query parameters">
<ParamsEditor value={value?.queryParams ?? []} onChange={onQueryParamsChange} /> <ParamsEditor value={value?.queryParams ?? []} onChange={onQueryParamsChange} />
</Field> </Field>
<Field label="Header parameters"> <Field label="Header parameters">
<ParamsEditor value={value?.headerParams ?? []} onChange={onHeaderParamsChange} /> <ParamsEditor value={value?.headerParams ?? []} onChange={onHeaderParamsChange} />
</Field> </Field>
{value?.method === HttpRequestMethod.POST && ( {value?.method === HttpRequestMethod.POST && value?.contentType && (
<> <Field label="Payload">
<InlineFieldRow> <StringValueEditor
<InlineField label="Content-Type" labelWidth={LABEL_WIDTH} grow={true}> context={context}
<Select value={value?.data ?? '{}'}
options={contentTypeOptions} onChange={onDataChange}
allowCustomValue={true} item={{ ...dummyStringSettings, settings: { useTextarea: true } }}
formatCreateLabel={formatCreateLabel} />
value={value?.contentType} </Field>
onChange={onContentTypeChange}
/>
</InlineField>
</InlineFieldRow>
{value?.contentType && (
<Field label="Payload">
<StringValueEditor
context={context}
value={value?.data ?? '{}'}
onChange={onDataChange}
item={{ ...dummyStringSettings, settings: { useTextarea: true } }}
/>
</Field>
)}
</>
)} )}
{renderTestAPIButton(value)} {renderTestAPIButton(value)}
<br /> <br />

View File

@ -7,11 +7,14 @@ import { HttpRequestMethod } from '../../panelcfg.gen';
import { APIEditorConfig } from './APIEditor'; import { APIEditorConfig } from './APIEditor';
export const callApi = (api: APIEditorConfig, isTest = false) => { type IsLoadingCallback = (loading: boolean) => void;
export const callApi = (api: APIEditorConfig, updateLoadingStateCallback?: IsLoadingCallback) => {
if (api && api.endpoint) { if (api && api.endpoint) {
// If API endpoint origin matches Grafana origin, don't call it. // If API endpoint origin matches Grafana origin, don't call it.
if (requestMatchesGrafanaOrigin(api.endpoint)) { if (requestMatchesGrafanaOrigin(api.endpoint)) {
appEvents.emit(AppEvents.alertError, ['Cannot call API at Grafana origin.']); appEvents.emit(AppEvents.alertError, ['Cannot call API at Grafana origin.']);
updateLoadingStateCallback && updateLoadingStateCallback(false);
return; return;
} }
const request = getRequest(api); const request = getRequest(api);
@ -20,15 +23,12 @@ export const callApi = (api: APIEditorConfig, isTest = false) => {
.fetch(request) .fetch(request)
.subscribe({ .subscribe({
error: (error) => { error: (error) => {
if (isTest) { appEvents.emit(AppEvents.alertError, ['An error has occurred: ', JSON.stringify(error)]);
appEvents.emit(AppEvents.alertError, ['Error has occurred: ', JSON.stringify(error)]); updateLoadingStateCallback && updateLoadingStateCallback(false);
console.error(error);
}
}, },
complete: () => { complete: () => {
if (isTest) { appEvents.emit(AppEvents.alertSuccess, ['API call was successful']);
appEvents.emit(AppEvents.alertSuccess, ['Test successful']); updateLoadingStateCallback && updateLoadingStateCallback(false);
}
}, },
}); });
} }