mirror of https://github.com/grafana/grafana.git
332 lines
9.5 KiB
TypeScript
332 lines
9.5 KiB
TypeScript
import { css } from '@emotion/css';
|
|
import { memo } from 'react';
|
|
|
|
import {
|
|
Action,
|
|
ActionType,
|
|
DataSourceInstanceSettings,
|
|
GrafanaTheme2,
|
|
httpMethodOptions,
|
|
HttpRequestMethod,
|
|
VariableSuggestion,
|
|
InfinityOptions,
|
|
FetchOptions,
|
|
ActionVariable,
|
|
} from '@grafana/data';
|
|
import { t } from '@grafana/i18n';
|
|
import {
|
|
ColorPicker,
|
|
Field,
|
|
InlineField,
|
|
InlineFieldRow,
|
|
JSONFormatter,
|
|
RadioButtonGroup,
|
|
Switch,
|
|
useStyles2,
|
|
useTheme2,
|
|
} from '@grafana/ui';
|
|
|
|
import { HTMLElementType, SuggestionsInput } from '../transformers/suggestionsInput/SuggestionsInput';
|
|
|
|
import { ActionVariablesEditor } from './ActionVariablesEditor';
|
|
import { ConnectionPicker } from './ConnectionPicker';
|
|
import { ParamsEditor } from './ParamsEditor';
|
|
|
|
interface ActionEditorProps {
|
|
index: number;
|
|
value: Action;
|
|
onChange: (index: number, action: Action) => void;
|
|
suggestions: VariableSuggestion[];
|
|
showOneClick?: boolean;
|
|
}
|
|
|
|
const LABEL_WIDTH = 13;
|
|
|
|
const DEFAULT_HTTP_CONFIG: FetchOptions = {
|
|
method: HttpRequestMethod.POST,
|
|
url: '',
|
|
body: '{}',
|
|
queryParams: [],
|
|
headers: [['Content-Type', 'application/json']],
|
|
};
|
|
|
|
export const ActionEditor = memo(({ index, value, onChange, suggestions, showOneClick }: ActionEditorProps) => {
|
|
const styles = useStyles2(getStyles);
|
|
const theme = useTheme2();
|
|
|
|
const getActionConfig = (): FetchOptions | InfinityOptions => {
|
|
if (value.type === ActionType.Infinity) {
|
|
return (
|
|
value[ActionType.Infinity] || {
|
|
...DEFAULT_HTTP_CONFIG,
|
|
datasourceUid: '',
|
|
}
|
|
);
|
|
}
|
|
|
|
return value[ActionType.Fetch] || DEFAULT_HTTP_CONFIG;
|
|
};
|
|
|
|
const updateActionConfig = (updates: Partial<FetchOptions | InfinityOptions>) => {
|
|
const configKey = value.type === ActionType.Infinity ? ActionType.Infinity : ActionType.Fetch;
|
|
const baseConfig = getActionConfig();
|
|
|
|
const updatedConfig = {
|
|
...baseConfig,
|
|
...updates,
|
|
...(value.type === ActionType.Infinity && {
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
datasourceUid: (baseConfig as InfinityOptions).datasourceUid || '',
|
|
}),
|
|
};
|
|
|
|
onChange(index, {
|
|
...value,
|
|
[configKey]: updatedConfig,
|
|
});
|
|
};
|
|
|
|
const updateConfig =
|
|
<K extends keyof (FetchOptions & InfinityOptions)>(field: K) =>
|
|
(newValue: (FetchOptions & InfinityOptions)[K]) => {
|
|
updateActionConfig({ [field]: newValue });
|
|
};
|
|
|
|
const onTitleChange = (title: string) => {
|
|
onChange(index, { ...value, title });
|
|
};
|
|
|
|
const onConfirmationChange = (confirmation: string) => {
|
|
onChange(index, { ...value, confirmation });
|
|
};
|
|
|
|
const onVariablesChange = (variables: ActionVariable[]) => {
|
|
onChange(index, {
|
|
...value,
|
|
variables,
|
|
});
|
|
};
|
|
|
|
const onOneClickChanged = () => {
|
|
onChange(index, { ...value, oneClick: !value.oneClick });
|
|
};
|
|
|
|
const onUrlChange = updateConfig('url');
|
|
const onBodyChange = updateConfig('body');
|
|
const onMethodChange = updateConfig('method');
|
|
const onQueryParamsChange = updateConfig('queryParams');
|
|
const onHeadersChange = updateConfig('headers');
|
|
|
|
const onBackgroundColorChange = (backgroundColor: string) => {
|
|
onChange(index, {
|
|
...value,
|
|
style: {
|
|
...value.style,
|
|
backgroundColor,
|
|
},
|
|
});
|
|
};
|
|
|
|
const onConnectionChange = (connectionType: string | DataSourceInstanceSettings) => {
|
|
const baseAction = {
|
|
title: value.title,
|
|
confirmation: value.confirmation,
|
|
oneClick: value.oneClick,
|
|
variables: value.variables,
|
|
style: value.style,
|
|
};
|
|
|
|
if (typeof connectionType === 'string') {
|
|
onChange(index, {
|
|
...baseAction,
|
|
type: ActionType.Fetch,
|
|
[ActionType.Fetch]: getActionConfig(),
|
|
});
|
|
} else {
|
|
onChange(index, {
|
|
...baseAction,
|
|
type: ActionType.Infinity,
|
|
[ActionType.Infinity]: {
|
|
...getActionConfig(),
|
|
datasourceUid: connectionType.uid,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
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 actionConfig = getActionConfig();
|
|
const shouldRenderJSON =
|
|
actionConfig.method !== HttpRequestMethod.GET &&
|
|
actionConfig.headers?.some(
|
|
([name, value]: [string, string]) => 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>
|
|
{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.connection', 'Connection')} labelWidth={LABEL_WIDTH}>
|
|
<ConnectionPicker
|
|
actionType={value.type}
|
|
datasourceUid={value?.[ActionType.Infinity]?.datasourceUid}
|
|
onChange={onConnectionChange}
|
|
/>
|
|
</InlineField>
|
|
</InlineFieldRow>
|
|
|
|
<InlineFieldRow>
|
|
<InlineField label={t('grafana-ui.action-editor.modal.action-method', 'Method')} labelWidth={LABEL_WIDTH}>
|
|
<RadioButtonGroup<HttpRequestMethod>
|
|
value={actionConfig.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={actionConfig.url}
|
|
onChange={onUrlChange}
|
|
suggestions={suggestions}
|
|
placeholder={t('actions.action-editor.placeholder-url', 'URL')}
|
|
/>
|
|
</InlineField>
|
|
</InlineFieldRow>
|
|
|
|
<Field
|
|
label={t('grafana-ui.action-editor.modal.action-variables', 'Variables')}
|
|
className={styles.fieldGap}
|
|
noMargin
|
|
>
|
|
<ActionVariablesEditor onChange={onVariablesChange} value={value.variables ?? []} />
|
|
</Field>
|
|
|
|
<Field
|
|
label={t('grafana-ui.action-editor.modal.action-query-params', 'Query parameters')}
|
|
className={styles.fieldGap}
|
|
>
|
|
<ParamsEditor value={actionConfig.queryParams ?? []} onChange={onQueryParamsChange} suggestions={suggestions} />
|
|
</Field>
|
|
|
|
<Field label={t('actions.action-editor.label-headers', 'Headers')}>
|
|
<ParamsEditor
|
|
value={actionConfig.headers ?? []}
|
|
onChange={onHeadersChange}
|
|
suggestions={suggestions}
|
|
contentTypeHeader={true}
|
|
/>
|
|
</Field>
|
|
|
|
{actionConfig.method !== HttpRequestMethod.GET && (
|
|
<Field label={t('grafana-ui.action-editor.modal.action-body', 'Body')} className={styles.inputField}>
|
|
<SuggestionsInput
|
|
value={actionConfig.body}
|
|
onChange={onBodyChange}
|
|
suggestions={suggestions}
|
|
type={HTMLElementType.TextAreaElement}
|
|
/>
|
|
</Field>
|
|
)}
|
|
|
|
{shouldRenderJSON && (
|
|
<>
|
|
<br />
|
|
{renderJSON(actionConfig.body)}
|
|
</>
|
|
)}
|
|
|
|
<Field label={t('grafana-ui.action-editor.button.style', 'Button style')} style={{ marginTop: '8px' }}>
|
|
<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>
|
|
</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';
|