grafana/public/app/features/actions/ActionEditor.tsx

267 lines
7.1 KiB
TypeScript

import { css } from '@emotion/css';
import { memo } from 'react';
import { Action, GrafanaTheme2, httpMethodOptions, HttpRequestMethod, VariableSuggestion } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import {
Switch,
Field,
InlineField,
InlineFieldRow,
RadioButtonGroup,
JSONFormatter,
useStyles2,
ColorPicker,
useTheme2,
} from '@grafana/ui';
import { HTMLElementType, SuggestionsInput } from '../transformers/suggestionsInput/SuggestionsInput';
import { ParamsEditor } from './ParamsEditor';
interface ActionEditorProps {
index: number;
value: Action;
onChange: (index: number, action: Action) => void;
suggestions: VariableSuggestion[];
showOneClick?: boolean;
}
const LABEL_WIDTH = 13;
export const ActionEditor = memo(({ index, value, onChange, suggestions, showOneClick }: ActionEditorProps) => {
const styles = useStyles2(getStyles);
const theme = useTheme2();
const { t } = useTranslate();
const onTitleChange = (title: string) => {
onChange(index, { ...value, title });
};
const onConfirmationChange = (confirmation: string) => {
onChange(index, { ...value, confirmation });
};
const onOneClickChanged = () => {
onChange(index, { ...value, oneClick: !value.oneClick });
};
const onUrlChange = (url: string) => {
onChange(index, {
...value,
fetch: {
...value.fetch,
url,
},
});
};
const onBodyChange = (body: string) => {
onChange(index, {
...value,
fetch: {
...value.fetch,
body,
},
});
};
const onMethodChange = (method: HttpRequestMethod) => {
onChange(index, {
...value,
fetch: {
...value.fetch,
method,
},
});
};
const onQueryParamsChange = (queryParams: Array<[string, string]>) => {
onChange(index, {
...value,
fetch: {
...value.fetch,
queryParams,
},
});
};
const onHeadersChange = (headers: Array<[string, string]>) => {
onChange(index, {
...value,
fetch: {
...value.fetch,
headers,
},
});
};
const onBackgroundColorChange = (backgroundColor: string) => {
onChange(index, {
...value,
style: {
...value.style,
backgroundColor,
},
});
};
const renderJSON = (data = '{}') => {
try {
const json = JSON.parse(data);
return <JSONFormatter json={json} />;
} catch (error) {
if (error instanceof Error) {
return `Invalid JSON provided: ${error.message}`;
} else {
return 'Invalid JSON provided';
}
}
};
const shouldRenderJSON =
value.fetch.method !== HttpRequestMethod.GET &&
value.fetch.headers?.some(([name, value]) => name === 'Content-Type' && value === 'application/json');
return (
<div className={styles.listItem}>
<Field label={t('grafana-ui.action-editor.modal.action-title', 'Title')} className={styles.inputField}>
<SuggestionsInput
value={value.title}
onChange={onTitleChange}
suggestions={suggestions}
autoFocus={value.title === ''}
placeholder={t('grafana-ui.action-editor.modal.action-title-placeholder', 'Action title')}
/>
</Field>
<Field
label={t('grafana-ui.viz-tooltip.actions-confirmation-label', 'Confirmation message')}
description={t(
'grafana-ui.viz-tooltip.actions-confirmation-message',
'Provide a descriptive prompt to confirm or cancel the action.'
)}
className={styles.inputField}
>
<SuggestionsInput
value={value.confirmation}
onChange={onConfirmationChange}
suggestions={suggestions}
placeholder={t(
'grafana-ui.viz-tooltip.actions-confirmation-input-placeholder',
'Are you sure you want to {{ actionTitle }}?',
{ actionTitle: value.title || '... ' }
)}
/>
</Field>
<Field label={t('grafana-ui.action-editor.button.style', 'Button style')}>
<InlineField
label={t('actions.action-editor.button.style.background-color', 'Color')}
labelWidth={LABEL_WIDTH}
className={styles.colorPicker}
>
<ColorPicker
color={value?.style?.backgroundColor || theme.colors.secondary.main}
onChange={onBackgroundColorChange}
/>
</InlineField>
</Field>
{showOneClick && (
<Field
label={t('grafana-ui.data-link-inline-editor.one-click', 'One click')}
description={t(
'grafana-ui.action-editor.modal.one-click-description',
'Only one link or action can have one click enabled at a time'
)}
>
<Switch value={value.oneClick || false} onChange={onOneClickChanged} />
</Field>
)}
<InlineFieldRow>
<InlineField
label={t('grafana-ui.action-editor.modal.action-method', 'Method')}
labelWidth={LABEL_WIDTH}
grow={true}
>
<RadioButtonGroup<HttpRequestMethod>
value={value?.fetch.method}
options={httpMethodOptions}
onChange={onMethodChange}
fullWidth
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label={t('actions.action-editor.label-url', 'URL')} labelWidth={LABEL_WIDTH} grow={true}>
<SuggestionsInput
value={value.fetch.url}
onChange={onUrlChange}
suggestions={suggestions}
placeholder={t('actions.action-editor.placeholder-url', 'URL')}
/>
</InlineField>
</InlineFieldRow>
<Field
label={t('grafana-ui.action-editor.modal.action-query-params', 'Query parameters')}
className={styles.fieldGap}
>
<ParamsEditor value={value?.fetch.queryParams ?? []} onChange={onQueryParamsChange} suggestions={suggestions} />
</Field>
<Field label={t('actions.action-editor.label-headers', 'Headers')}>
<ParamsEditor
value={value?.fetch.headers ?? []}
onChange={onHeadersChange}
suggestions={suggestions}
contentTypeHeader={true}
/>
</Field>
{value?.fetch.method !== HttpRequestMethod.GET && (
<Field label={t('grafana-ui.action-editor.modal.action-body', 'Body')} className={styles.inputField}>
<SuggestionsInput
value={value.fetch.body}
onChange={onBodyChange}
suggestions={suggestions}
type={HTMLElementType.TextAreaElement}
/>
</Field>
)}
{shouldRenderJSON && (
<>
<br />
{renderJSON(value?.fetch.body)}
</>
)}
</div>
);
});
const getStyles = (theme: GrafanaTheme2) => ({
listItem: css({
marginBottom: theme.spacing(),
}),
infoText: css({
paddingBottom: theme.spacing(2),
marginLeft: '66px',
color: theme.colors.text.secondary,
}),
fieldGap: css({
marginTop: theme.spacing(2),
}),
inputField: css({
marginRight: 4,
}),
colorPicker: css({
display: 'flex',
alignItems: 'center',
}),
});
ActionEditor.displayName = 'ActionEditor';