remove old webhook UI (#2536)

# What this PR does

remove old webhook UI


## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [ ] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
This commit is contained in:
Joey Orlando 2023-07-31 17:18:41 +02:00 committed by GitHub
parent 065f9aea7f
commit 9c13acb9f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 638 additions and 1261 deletions

View file

@ -22,7 +22,6 @@ import {
} from 'models/escalation_policy/escalation_policy.types';
import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook';
import { OutgoingWebhook2Store } from 'models/outgoing_webhook_2/outgoing_webhook_2';
import { ScheduleStore } from 'models/schedule/schedule';
import { WaitDelay } from 'models/wait_delay';
import { SelectOption } from 'state/types';
@ -54,7 +53,6 @@ export interface EscalationPolicyProps extends ElementSortableProps {
isSlackInstalled: boolean;
teamStore: GrafanaTeamStore;
outgoingWebhookStore: OutgoingWebhookStore;
outgoingWebhook2Store: OutgoingWebhook2Store;
scheduleStore: ScheduleStore;
}
@ -111,8 +109,6 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
return this._renderNotifyUserGroup();
case 'schedule':
return this._renderNotifySchedule();
case 'custom_action':
return this._renderTriggerCustomAction();
case 'custom_webhook':
return this._renderTriggerCustomWebhook();
case 'num_alerts_in_window':
@ -381,39 +377,8 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
);
}
private _renderTriggerCustomAction() {
const { data, isDisabled, teamStore, outgoingWebhookStore } = this.props;
const { custom_button_trigger } = data;
return (
<WithPermissionControlTooltip key="custom-button" disableByPaywall userAction={UserActions.EscalationChainsWrite}>
<GSelect
showSearch
disabled={isDisabled}
modelName="outgoingWebhookStore"
displayField="name"
valueField="id"
placeholder="Select Webhook"
className={cx('select', 'control')}
value={custom_button_trigger}
onChange={this._getOnChangeHandler('custom_button_trigger')}
getOptionLabel={(item: SelectableValue) => {
const team = teamStore.items[outgoingWebhookStore.items[item.value].team];
return (
<>
<Text>{item.label} </Text>
<TeamName team={team} size="small" />
</>
);
}}
width={'auto'}
/>
</WithPermissionControlTooltip>
);
}
private _renderTriggerCustomWebhook() {
const { data, isDisabled, teamStore, outgoingWebhook2Store } = this.props;
const { data, isDisabled, teamStore, outgoingWebhookStore } = this.props;
const { custom_webhook } = data;
return (
@ -425,7 +390,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
<GSelect
showSearch
disabled={isDisabled}
modelName="outgoingWebhook2Store"
modelName="outgoingWebhookStore"
displayField="name"
valueField="id"
placeholder="Select Webhook"
@ -433,7 +398,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
value={custom_webhook}
onChange={this._getOnChangeHandler('custom_webhook')}
getOptionLabel={(item: SelectableValue) => {
const team = teamStore.items[outgoingWebhook2Store.items[item.value].team];
const team = teamStore.items[outgoingWebhookStore.items[item.value].team];
return (
<>
<Text>{item.label} </Text>
@ -443,7 +408,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
}}
width={'auto'}
filterOptions={(id) => {
const webhook = outgoingWebhook2Store.items[id];
const webhook = outgoingWebhookStore.items[id];
return webhook.trigger_type_name === 'Escalation step';
}}
/>

View file

@ -12,7 +12,6 @@ import Text from 'components/Text/Text';
import TeamName from 'containers/TeamName/TeamName';
import { HeartGreenIcon, HeartRedIcon } from 'icons';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { AppFeature } from 'state/features';
import { useStore } from 'state/useStore';
import styles from './AlertReceiveChannelCard.module.scss';
@ -65,22 +64,20 @@ const AlertReceiveChannelCard = observer((props: AlertReceiveChannelCardProps) =
<Text type="primary" size="medium">
<Emoji className={cx('title')} text={alertReceiveChannel.verbal_name} />
</Text>
{store.hasFeature(AppFeature.Webhooks2) && (
<CopyToClipboard text={alertReceiveChannel.id}>
<IconButton
variant="primary"
tooltip={
<div>
ID {alertReceiveChannel.id}
<br />
(click to copy ID to clipboard)
</div>
}
tooltipPlacement="top"
name="info-circle"
/>
</CopyToClipboard>
)}
<CopyToClipboard text={alertReceiveChannel.id}>
<IconButton
variant="primary"
tooltip={
<div>
ID {alertReceiveChannel.id}
<br />
(click to copy ID to clipboard)
</div>
}
tooltipPlacement="top"
name="info-circle"
/>
</CopyToClipboard>
{alertReceiveChannelCounter && (
<PluginLink
query={{ page: 'alert-groups', integration: alertReceiveChannel.id }}

View file

@ -90,7 +90,6 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
teamStore={store.grafanaTeamStore}
scheduleStore={store.scheduleStore}
outgoingWebhookStore={store.outgoingWebhookStore}
outgoingWebhook2Store={store.outgoingWebhook2Store}
isDisabled={isDisabled}
/>
);

View file

@ -1,30 +0,0 @@
.root {
display: block;
}
.title {
margin: 16px 0 0 16px;
}
.content {
margin: 4px;
}
.tabs__content {
padding-top: 16px;
}
.form-row {
display: flex;
flex-wrap: nowrap;
gap: 4px;
}
.form-field {
flex-grow: 1;
}
/* TODO: figure out why this is not picked */
.webhooks__drawerContent .cursor.monaco-mouse-cursor-text {
display: none !important;
}

View file

@ -1,312 +0,0 @@
import React, { useCallback, useState } from 'react';
import { Button, ConfirmModal, ConfirmModalProps, Drawer, HorizontalGroup, Tab, TabsBar } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import { useHistory } from 'react-router-dom';
import GForm from 'components/GForm/GForm';
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
import Text from 'components/Text/Text';
import OutgoingWebhook2Status from 'containers/OutgoingWebhook2Status/OutgoingWebhook2Status';
import WebhooksTemplateEditor from 'containers/WebhooksTemplateEditor/WebhooksTemplateEditor';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { OutgoingWebhook2 } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
import { WebhookFormActionType } from 'pages/outgoing_webhooks_2/OutgoingWebhooks2.types';
import { useStore } from 'state/useStore';
import { KeyValuePair } from 'utils';
import { UserActions } from 'utils/authorization';
import { PLUGIN_ROOT } from 'utils/consts';
import { form } from './OutgoingWebhook2Form.config';
import styles from 'containers/OutgoingWebhook2Form/OutgoingWebhook2Form.module.css';
const cx = cn.bind(styles);
interface OutgoingWebhook2FormProps {
id: OutgoingWebhook2['id'] | 'new';
action: WebhookFormActionType;
onHide: () => void;
onUpdate: () => void;
onDelete: () => void;
}
export const WebhookTabs = {
Settings: new KeyValuePair('Settings', 'Settings'),
LastRun: new KeyValuePair('LastRun', 'Last Run'),
};
const OutgoingWebhook2Form = observer((props: OutgoingWebhook2FormProps) => {
const history = useHistory();
const { id, action, onUpdate, onHide, onDelete } = props;
const [onFormChangeFn, setOnFormChangeFn] = useState<{ fn: (value: string) => void }>(undefined);
const [templateToEdit, setTemplateToEdit] = useState(undefined);
const [activeTab, setActiveTab] = useState<string>(
action === WebhookFormActionType.EDIT_SETTINGS ? WebhookTabs.Settings.key : WebhookTabs.LastRun.key
);
const { outgoingWebhook2Store } = useStore();
const isNew = action === WebhookFormActionType.NEW;
const isNewOrCopy = isNew || action === WebhookFormActionType.COPY;
const handleSubmit = useCallback(
(data: Partial<OutgoingWebhook2>) => {
(isNewOrCopy ? outgoingWebhook2Store.create(data) : outgoingWebhook2Store.update(id, data)).then(() => {
onHide();
onUpdate();
});
},
[id]
);
const getTemplateEditClickHandler = (formItem: FormItem, values, setFormFieldValue) => {
return () => {
const formValue = values[formItem.name];
setTemplateToEdit({ value: formValue, displayName: undefined, description: undefined, name: formItem.name });
setOnFormChangeFn({ fn: (value) => setFormFieldValue(value) });
};
};
const enrchField = (
formItem: FormItem,
disabled: boolean,
renderedControl: React.ReactElement,
values,
setFormFieldValue
) => {
if (formItem.type === FormItemType.Monaco) {
return (
<div className={cx('form-row')}>
<div className={cx('form-field')}>{renderedControl}</div>
<Button
disabled={disabled}
icon="edit"
variant="secondary"
onClick={getTemplateEditClickHandler(formItem, values, setFormFieldValue)}
/>
</div>
);
}
return renderedControl;
};
if (
(action === WebhookFormActionType.EDIT_SETTINGS || action === WebhookFormActionType.VIEW_LAST_RUN) &&
!outgoingWebhook2Store.items[id]
) {
return null;
}
let data:
| OutgoingWebhook2
| {
is_webhook_enabled: boolean;
is_legacy: boolean;
};
if (isNew) {
data = { is_webhook_enabled: true, is_legacy: false };
} else if (isNewOrCopy) {
data = { ...outgoingWebhook2Store.items[id], is_legacy: false, name: '' };
} else {
data = outgoingWebhook2Store.items[id];
}
if (
(action === WebhookFormActionType.EDIT_SETTINGS || action === WebhookFormActionType.VIEW_LAST_RUN) &&
!outgoingWebhook2Store.items[id]
) {
// nothing to show if we open invalid ID for edit/last_run
return null;
}
const formElement = <GForm form={form} data={data} onSubmit={handleSubmit} onFieldRender={enrchField} />;
if (action === WebhookFormActionType.NEW || action === WebhookFormActionType.COPY) {
// show just the creation form, not the tabs
return (
<>
<Drawer scrollableContent title={'Create Outgoing Webhook'} onClose={onHide} closeOnMaskClick={false}>
<div className="webhooks__drawerContent">{renderWebhookForm()}</div>
</Drawer>
{templateToEdit && (
<WebhooksTemplateEditor
id={id}
handleSubmit={(value) => {
onFormChangeFn?.fn(value);
setTemplateToEdit(undefined);
}}
onHide={() => setTemplateToEdit(undefined)}
template={templateToEdit}
/>
)}
</>
);
}
return (
// show tabbed drawer (edit/live_run)
<>
<Drawer scrollableContent title={'Outgoing webhook details'} onClose={onHide} closeOnMaskClick={false}>
<div className={cx('webhooks__drawerContent')}>
<TabsBar>
<Tab
key={WebhookTabs.Settings.key}
onChangeTab={() => {
setActiveTab(WebhookTabs.Settings.key);
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`);
}}
active={activeTab === WebhookTabs.Settings.key}
label={WebhookTabs.Settings.value}
/>
<Tab
key={WebhookTabs.LastRun.key}
onChangeTab={() => {
setActiveTab(WebhookTabs.LastRun.key);
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`);
}}
active={activeTab === WebhookTabs.LastRun.key}
label={WebhookTabs.LastRun.value}
/>
</TabsBar>
<WebhookTabsContent
id={id}
action={action}
activeTab={activeTab}
data={data}
handleSubmit={handleSubmit}
onDelete={onDelete}
onHide={onHide}
onUpdate={onUpdate}
formElement={formElement}
/>
</div>
</Drawer>
{templateToEdit && (
<WebhooksTemplateEditor
id={id}
handleSubmit={(value) => {
onFormChangeFn?.fn(value);
setTemplateToEdit(undefined);
}}
onHide={() => setTemplateToEdit(undefined)}
template={templateToEdit}
/>
)}
</>
);
function renderWebhookForm() {
return (
<>
<div className={cx('content')} data-testid="test__outgoingWebhook2EditForm">
<GForm form={form} data={data} onSubmit={handleSubmit} onFieldRender={enrchField} />
<div className={cx('buttons')}>
<HorizontalGroup justify={'flex-end'}>
<Button variant="secondary" onClick={onHide}>
Cancel
</Button>
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
<Button form={form.name} type="submit" disabled={data.is_legacy}>
{isNewOrCopy ? 'Create' : 'Update'} Webhook
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
</div>
</div>
</>
);
}
});
interface WebhookTabsProps {
id: OutgoingWebhook2['id'] | 'new';
activeTab: string;
action: WebhookFormActionType;
data:
| OutgoingWebhook2
| {
is_webhook_enabled: boolean;
is_legacy: boolean;
};
onHide: () => void;
onUpdate: () => void;
onDelete: () => void;
handleSubmit: (data: Partial<OutgoingWebhook2>) => void;
formElement: React.ReactElement;
}
const WebhookTabsContent: React.FC<WebhookTabsProps> = ({
id,
action,
activeTab,
data,
onHide,
onUpdate,
onDelete,
formElement,
}) => {
const [confirmationModal, setConfirmationModal] = useState<ConfirmModalProps>(undefined);
return (
<div className={cx('tabs__content')}>
{confirmationModal && (
<ConfirmModal {...(confirmationModal as ConfirmModalProps)} onDismiss={() => setConfirmationModal(undefined)} />
)}
{activeTab === WebhookTabs.Settings.key && (
<>
<div className={cx('content')} data-testid="test__outgoingWebhook2EditForm">
{formElement}
<div className={cx('buttons')}>
<HorizontalGroup justify={'flex-end'}>
<Button variant="secondary" onClick={onHide}>
Cancel
</Button>
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
<Button
form={form.name}
variant="destructive"
type="button"
disabled={data.is_legacy}
onClick={() => {
setConfirmationModal({
isOpen: true,
body: 'The action cannot be undone.',
confirmText: 'Delete',
dismissText: 'Cancel',
onConfirm: onDelete,
title: `Are you sure you want to delete webhook?`,
} as ConfirmModalProps);
}}
>
Delete Webhook
</Button>
</WithPermissionControlTooltip>
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
<Button form={form.name} type="submit" disabled={data.is_legacy}>
{action === WebhookFormActionType.NEW ? 'Create' : 'Update'} Webhook
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
</div>
</div>
{data.is_legacy ? (
<div className={cx('content')}>
<Text type="secondary">Legacy migrated webhooks are not editable. Make a copy to make changes.</Text>
</div>
) : (
''
)}
</>
)}
{activeTab === WebhookTabs.LastRun.key && <OutgoingWebhook2Status id={id} onUpdate={onUpdate} />}
</div>
);
};
export default OutgoingWebhook2Form;

View file

@ -1,62 +0,0 @@
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
export const form: { name: string; fields: FormItem[] } = {
name: 'OutgoingWebhook',
fields: [
{
name: 'name',
type: FormItemType.Input,
validation: { required: true },
},
{
name: 'team',
label: 'Assign to team',
description:
'Assigning to the teams allows you to filter Outgoing Webhooks and configure their visibility. Go to OnCall -> Settings -> Team and Access Settings for more details',
type: FormItemType.GSelect,
extra: {
modelName: 'grafanaTeamStore',
displayField: 'name',
valueField: 'id',
showSearch: true,
allowClear: true,
},
},
{
name: 'webhook',
label: 'Webhook URL',
type: FormItemType.Input,
validation: { required: true },
},
{
name: 'user',
type: FormItemType.Input,
},
{
name: 'password',
type: FormItemType.Input,
},
{
name: 'authorization_header',
type: FormItemType.TextArea,
extra: {
rows: 5,
},
},
{
name: 'data',
getDisabled: (form_data) => Boolean(form_data?.forward_whole_payload),
type: FormItemType.TextArea,
description: 'Available variables: {{ alert_payload }}, {{ alert_group_id }}',
extra: {
rows: 9,
},
},
{
name: 'forward_whole_payload',
normalize: (value) => Boolean(value),
type: FormItemType.Switch,
description: "Forwards whole payload of the alert to the webhook's url as POST data",
},
],
};

View file

@ -18,7 +18,7 @@ export const WebhookTriggerType = {
};
export const form: { name: string; fields: FormItem[] } = {
name: 'OutgoingWebhook2',
name: 'OutgoingWebhook',
fields: [
{
name: 'name',

View file

@ -9,3 +9,22 @@
.content {
margin: 4px;
}
.tabs__content {
padding-top: 16px;
}
.form-row {
display: flex;
flex-wrap: nowrap;
gap: 4px;
}
.form-field {
flex-grow: 1;
}
/* TODO: figure out why this is not picked */
.webhooks__drawerContent .cursor.monaco-mouse-cursor-text {
display: none !important;
}

View file

@ -1,14 +1,22 @@
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { Button, Drawer, HorizontalGroup } from '@grafana/ui';
import { Button, ConfirmModal, ConfirmModalProps, Drawer, HorizontalGroup, Tab, TabsBar } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import { useHistory } from 'react-router-dom';
import GForm from 'components/GForm/GForm';
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
import Text from 'components/Text/Text';
import OutgoingWebhookStatus from 'containers/OutgoingWebhookStatus/OutgoingWebhookStatus';
import WebhooksTemplateEditor from 'containers/WebhooksTemplateEditor/WebhooksTemplateEditor';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
import { WebhookFormActionType } from 'pages/outgoing_webhooks/OutgoingWebhooks.types';
import { useStore } from 'state/useStore';
import { KeyValuePair } from 'utils';
import { UserActions } from 'utils/authorization';
import { PLUGIN_ROOT } from 'utils/consts';
import { form } from './OutgoingWebhookForm.config';
@ -18,53 +26,287 @@ const cx = cn.bind(styles);
interface OutgoingWebhookFormProps {
id: OutgoingWebhook['id'] | 'new';
action: WebhookFormActionType;
onHide: () => void;
onUpdate: () => void;
onDelete: () => void;
}
export const WebhookTabs = {
Settings: new KeyValuePair('Settings', 'Settings'),
LastRun: new KeyValuePair('LastRun', 'Last Run'),
};
const OutgoingWebhookForm = observer((props: OutgoingWebhookFormProps) => {
const { id, onUpdate, onHide } = props;
const history = useHistory();
const { id, action, onUpdate, onHide, onDelete } = props;
const [onFormChangeFn, setOnFormChangeFn] = useState<{ fn: (value: string) => void }>(undefined);
const [templateToEdit, setTemplateToEdit] = useState(undefined);
const [activeTab, setActiveTab] = useState<string>(
action === WebhookFormActionType.EDIT_SETTINGS ? WebhookTabs.Settings.key : WebhookTabs.LastRun.key
);
const store = useStore();
const { outgoingWebhookStore, userStore } = store;
const user = userStore.currentUser;
const data = id === 'new' ? { team: user.current_team } : outgoingWebhookStore.items[id];
const { outgoingWebhookStore } = useStore();
const isNew = action === WebhookFormActionType.NEW;
const isNewOrCopy = isNew || action === WebhookFormActionType.COPY;
const handleSubmit = useCallback(
(data: Partial<OutgoingWebhook>) => {
(id === 'new' ? outgoingWebhookStore.create(data) : outgoingWebhookStore.update(id, data)).then(() => {
(isNewOrCopy ? outgoingWebhookStore.create(data) : outgoingWebhookStore.update(id, data)).then(() => {
onHide();
onUpdate();
});
},
[id]
);
const getTemplateEditClickHandler = (formItem: FormItem, values, setFormFieldValue) => {
return () => {
const formValue = values[formItem.name];
setTemplateToEdit({ value: formValue, displayName: undefined, description: undefined, name: formItem.name });
setOnFormChangeFn({ fn: (value) => setFormFieldValue(value) });
};
};
const enrchField = (
formItem: FormItem,
disabled: boolean,
renderedControl: React.ReactElement,
values,
setFormFieldValue
) => {
if (formItem.type === FormItemType.Monaco) {
return (
<div className={cx('form-row')}>
<div className={cx('form-field')}>{renderedControl}</div>
<Button
disabled={disabled}
icon="edit"
variant="secondary"
onClick={getTemplateEditClickHandler(formItem, values, setFormFieldValue)}
/>
</div>
);
}
return renderedControl;
};
if (
(action === WebhookFormActionType.EDIT_SETTINGS || action === WebhookFormActionType.VIEW_LAST_RUN) &&
!outgoingWebhookStore.items[id]
) {
return null;
}
let data:
| OutgoingWebhook
| {
is_webhook_enabled: boolean;
is_legacy: boolean;
};
if (isNew) {
data = { is_webhook_enabled: true, is_legacy: false };
} else if (isNewOrCopy) {
data = { ...outgoingWebhookStore.items[id], is_legacy: false, name: '' };
} else {
data = outgoingWebhookStore.items[id];
}
if (
(action === WebhookFormActionType.EDIT_SETTINGS || action === WebhookFormActionType.VIEW_LAST_RUN) &&
!outgoingWebhookStore.items[id]
) {
// nothing to show if we open invalid ID for edit/last_run
return null;
}
const formElement = <GForm form={form} data={data} onSubmit={handleSubmit} onFieldRender={enrchField} />;
if (action === WebhookFormActionType.NEW || action === WebhookFormActionType.COPY) {
// show just the creation form, not the tabs
return (
<>
<Drawer scrollableContent title={'Create Outgoing Webhook'} onClose={onHide} closeOnMaskClick={false}>
<div className="webhooks__drawerContent">{renderWebhookForm()}</div>
</Drawer>
{templateToEdit && (
<WebhooksTemplateEditor
id={id}
handleSubmit={(value) => {
onFormChangeFn?.fn(value);
setTemplateToEdit(undefined);
}}
onHide={() => setTemplateToEdit(undefined)}
template={templateToEdit}
/>
)}
</>
);
}
return (
<Drawer
scrollableContent
title={id === 'new' ? 'Create Outgoing Webhook' : 'Edit Outgoing Webhook'}
onClose={onHide}
closeOnMaskClick={false}
>
<div className={cx('content')} data-testid="test__outgoingWebhookEditForm">
<GForm form={form} data={data} onSubmit={handleSubmit} />
<HorizontalGroup justify="flex-end">
<Button variant="secondary" onClick={onHide}>
Cancel
</Button>
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
<Button form={form.name} type="submit">
{id === 'new' ? 'Create' : 'Update'} Webhook
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
</div>
</Drawer>
// show tabbed drawer (edit/live_run)
<>
<Drawer scrollableContent title={'Outgoing webhook details'} onClose={onHide} closeOnMaskClick={false}>
<div className={cx('webhooks__drawerContent')}>
<TabsBar>
<Tab
key={WebhookTabs.Settings.key}
onChangeTab={() => {
setActiveTab(WebhookTabs.Settings.key);
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`);
}}
active={activeTab === WebhookTabs.Settings.key}
label={WebhookTabs.Settings.value}
/>
<Tab
key={WebhookTabs.LastRun.key}
onChangeTab={() => {
setActiveTab(WebhookTabs.LastRun.key);
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`);
}}
active={activeTab === WebhookTabs.LastRun.key}
label={WebhookTabs.LastRun.value}
/>
</TabsBar>
<WebhookTabsContent
id={id}
action={action}
activeTab={activeTab}
data={data}
handleSubmit={handleSubmit}
onDelete={onDelete}
onHide={onHide}
onUpdate={onUpdate}
formElement={formElement}
/>
</div>
</Drawer>
{templateToEdit && (
<WebhooksTemplateEditor
id={id}
handleSubmit={(value) => {
onFormChangeFn?.fn(value);
setTemplateToEdit(undefined);
}}
onHide={() => setTemplateToEdit(undefined)}
template={templateToEdit}
/>
)}
</>
);
function renderWebhookForm() {
return (
<>
<div className={cx('content')}>
<GForm form={form} data={data} onSubmit={handleSubmit} onFieldRender={enrchField} />
<div className={cx('buttons')}>
<HorizontalGroup justify={'flex-end'}>
<Button variant="secondary" onClick={onHide}>
Cancel
</Button>
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
<Button form={form.name} type="submit" disabled={data.is_legacy}>
{isNewOrCopy ? 'Create' : 'Update'} Webhook
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
</div>
</div>
</>
);
}
});
interface WebhookTabsProps {
id: OutgoingWebhook['id'] | 'new';
activeTab: string;
action: WebhookFormActionType;
data:
| OutgoingWebhook
| {
is_webhook_enabled: boolean;
is_legacy: boolean;
};
onHide: () => void;
onUpdate: () => void;
onDelete: () => void;
handleSubmit: (data: Partial<OutgoingWebhook>) => void;
formElement: React.ReactElement;
}
const WebhookTabsContent: React.FC<WebhookTabsProps> = ({
id,
action,
activeTab,
data,
onHide,
onUpdate,
onDelete,
formElement,
}) => {
const [confirmationModal, setConfirmationModal] = useState<ConfirmModalProps>(undefined);
return (
<div className={cx('tabs__content')}>
{confirmationModal && (
<ConfirmModal {...(confirmationModal as ConfirmModalProps)} onDismiss={() => setConfirmationModal(undefined)} />
)}
{activeTab === WebhookTabs.Settings.key && (
<>
<div className={cx('content')}>
{formElement}
<div className={cx('buttons')}>
<HorizontalGroup justify={'flex-end'}>
<Button variant="secondary" onClick={onHide}>
Cancel
</Button>
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
<Button
form={form.name}
variant="destructive"
type="button"
disabled={data.is_legacy}
onClick={() => {
setConfirmationModal({
isOpen: true,
body: 'The action cannot be undone.',
confirmText: 'Delete',
dismissText: 'Cancel',
onConfirm: onDelete,
title: `Are you sure you want to delete webhook?`,
} as ConfirmModalProps);
}}
>
Delete Webhook
</Button>
</WithPermissionControlTooltip>
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
<Button form={form.name} type="submit" disabled={data.is_legacy}>
{action === WebhookFormActionType.NEW ? 'Create' : 'Update'} Webhook
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
</div>
</div>
{data.is_legacy ? (
<div className={cx('content')}>
<Text type="secondary">Legacy migrated webhooks are not editable. Make a copy to make changes.</Text>
</div>
) : (
''
)}
</>
)}
{activeTab === WebhookTabs.LastRun.key && <OutgoingWebhookStatus id={id} onUpdate={onUpdate} />}
</div>
);
};
export default OutgoingWebhookForm;

View file

@ -7,15 +7,15 @@ import { observer } from 'mobx-react';
import Block from 'components/GBlock/Block';
import SourceCode from 'components/SourceCode/SourceCode';
import Text from 'components/Text/Text';
import { OutgoingWebhook2 } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
import { useStore } from 'state/useStore';
import styles from 'containers/OutgoingWebhook2Form/OutgoingWebhook2Form.module.css';
import styles from 'containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css';
const cx = cn.bind(styles);
interface OutgoingWebhook2StatusProps {
id: OutgoingWebhook2['id'];
interface OutgoingWebhookStatusProps {
id: OutgoingWebhook['id'];
onUpdate: () => void;
}
@ -47,14 +47,14 @@ function format_response_field(str) {
}
}
const OutgoingWebhook2Status = observer((props: OutgoingWebhook2StatusProps) => {
const OutgoingWebhookStatus = observer((props: OutgoingWebhookStatusProps) => {
const { id } = props;
const store = useStore();
const { outgoingWebhook2Store } = store;
const { outgoingWebhookStore } = store;
const data = outgoingWebhook2Store.items[id];
const data = outgoingWebhookStore.items[id];
return (
<div className={cx('content')}>
@ -119,4 +119,4 @@ const OutgoingWebhook2Status = observer((props: OutgoingWebhook2StatusProps) =>
);
});
export default OutgoingWebhook2Status;
export default OutgoingWebhookStatus;

View file

@ -7,7 +7,7 @@ import { observer } from 'mobx-react';
import Text from 'components/Text/Text';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { Alert } from 'models/alertgroup/alertgroup.types';
import { OutgoingWebhook2 } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
import { useStore } from 'state/useStore';
import { openErrorNotification } from 'utils';
import { useDebouncedCallback } from 'utils/hooks';
@ -25,7 +25,7 @@ interface TemplatePreviewProps {
payload?: JSON;
alertReceiveChannelId: AlertReceiveChannel['id'];
alertGroupId?: Alert['pk'];
outgoingWebhookId?: OutgoingWebhook2['id'];
outgoingWebhookId?: OutgoingWebhook['id'];
templatePage: TEMPLATE_PAGE;
}
interface ConditionalResult {
@ -55,11 +55,11 @@ const TemplatePreview = observer((props: TemplatePreviewProps) => {
const [conditionalResult, setConditionalResult] = useState<ConditionalResult>({});
const store = useStore();
const { alertReceiveChannelStore, alertGroupStore, outgoingWebhook2Store } = store;
const { alertReceiveChannelStore, alertGroupStore, outgoingWebhookStore } = store;
const handleTemplateBodyChange = useDebouncedCallback(() => {
(templatePage === TEMPLATE_PAGE.Webhooks
? outgoingWebhook2Store.renderPreview(outgoingWebhookId, templateName, templateBody, payload)
? outgoingWebhookStore.renderPreview(outgoingWebhookId, templateName, templateBody, payload)
: alertGroupId
? alertGroupStore.renderPreview(alertGroupId, templateName, templateBody)
: alertReceiveChannelStore.renderPreview(alertReceiveChannelId, templateName, templateBody, payload)

View file

@ -9,13 +9,13 @@ import Text from 'components/Text/Text';
import styles from 'containers/IntegrationTemplate/IntegrationTemplate.module.scss';
import TemplatePreview, { TEMPLATE_PAGE } from 'containers/TemplatePreview/TemplatePreview';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { OutgoingWebhook2 } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
const cx = cn.bind(styles);
interface ResultProps {
alertReceiveChannelId?: AlertReceiveChannel['id'];
outgoingWebhookId?: OutgoingWebhook2['id'];
outgoingWebhookId?: OutgoingWebhook['id'];
templateBody: string;
template: TemplateForEdit;
isAlertGroupExisting?: boolean;

View file

@ -11,7 +11,7 @@ import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { AlertTemplatesDTO } from 'models/alert_templates';
import { Alert } from 'models/alertgroup/alertgroup.types';
import { OutgoingWebhook2, OutgoingWebhook2Response } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
import { OutgoingWebhook, OutgoingWebhookResponse } from 'models/outgoing_webhook/outgoing_webhook.types';
import { useStore } from 'state/useStore';
import styles from './TemplatesAlertGroupsList.module.css';
@ -29,7 +29,7 @@ interface TemplatesAlertGroupsListProps {
templatePage: TEMPLATE_PAGE;
templates: AlertTemplatesDTO[];
alertReceiveChannelId?: AlertReceiveChannel['id'];
outgoingwebhookId?: OutgoingWebhook2['id'];
outgoingwebhookId?: OutgoingWebhook['id'];
heading?: string;
onSelectAlertGroup?: (alertGroup: Alert) => void;
@ -52,7 +52,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
const store = useStore();
const [alertGroupsList, setAlertGroupsList] = useState(undefined);
const [outgoingWebhookLastResponses, setOutgoingWebhookLastResponses] =
useState<OutgoingWebhook2Response[]>(undefined);
useState<OutgoingWebhookResponse[]>(undefined);
const [selectedTitle, setSelectedTitle] = useState<string>(undefined);
const [selectedPayload, setSelectedPayload] = useState<string>(undefined);
@ -61,7 +61,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
useEffect(() => {
if (templatePage === TEMPLATE_PAGE.Webhooks) {
if (outgoingwebhookId !== 'new') {
store.outgoingWebhook2Store.getLastResponses(outgoingwebhookId).then(setOutgoingWebhookLastResponses);
store.outgoingWebhookStore.getLastResponses(outgoingwebhookId).then(setOutgoingWebhookLastResponses);
}
} else if (templatePage === TEMPLATE_PAGE.Integrations) {
store.alertGroupStore.getAlertGroupsForIntegration(alertReceiveChannelId).then((result) => {
@ -117,7 +117,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
// for Outgoing webhooks
const handleOutgoingWebhookResponseSelect = (response: OutgoingWebhook2Response) => {
const handleOutgoingWebhookResponseSelect = (response: OutgoingWebhookResponse) => {
setSelectedTitle(response.timestamp);
setSelectedPayload(JSON.parse(response.event_data));

View file

@ -12,7 +12,7 @@ import styles from 'containers/IntegrationTemplate/IntegrationTemplate.module.sc
import TemplateResult from 'containers/TemplateResult/TemplateResult';
import TemplatesAlertGroupsList, { TEMPLATE_PAGE } from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { OutgoingWebhook2 } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
import { waitForElement } from 'utils/DOM';
import { UserActions } from 'utils/authorization';
@ -27,7 +27,7 @@ interface Template {
interface WebhooksTemplateEditorProps {
template: Template;
id: OutgoingWebhook2['id'];
id: OutgoingWebhook['id'];
onHide: () => void;
handleSubmit: (template: string) => void;
}

View file

@ -1,12 +0,0 @@
import { AlertReceiveChannel } from './alert_receive_channel/alert_receive_channel.types';
export interface ActionDTO {
id: string;
name: string;
webhook: string;
user: string;
password: string;
alert_receive_channel: AlertReceiveChannel['id'];
data: string;
authorization_header: string;
}

View file

@ -1,7 +1,6 @@
import { omit } from 'lodash-es';
import { action, observable } from 'mobx';
import { ActionDTO } from 'models/action';
import { AlertTemplatesDTO } from 'models/alert_templates';
import { Alert } from 'models/alertgroup/alertgroup.types';
import BaseStore from 'models/base_store';
@ -359,28 +358,6 @@ export class AlertReceiveChannelStore extends BaseStore {
};
}
@action
async updateCustomButtons(alertReceiveChannelId: AlertReceiveChannel['id']) {
const response = await makeRequest(`/custom_buttons/`, {
params: {
alert_receive_channel: alertReceiveChannelId,
},
withCredentials: true,
});
this.actions = {
...this.actions,
[alertReceiveChannelId]: response,
};
}
async deleteCustomButton(id: ActionDTO['id']) {
await makeRequest(`/custom_buttons/${id}/`, {
method: 'DELETE',
withCredentials: true,
});
}
async getAccessLogs(alertReceiveChannelId: AlertReceiveChannel['id']) {
const { integration_log } = await makeRequest(`/alert_receive_channel_access_log/${alertReceiveChannelId}/`, {});

View file

@ -2,7 +2,6 @@ import { Channel } from 'models/channel';
import { Schedule } from 'models/schedule/schedule.types';
import { UserGroup } from 'models/user_group/user_group.types';
import { ActionDTO } from './action';
import { ChannelFilter } from './channel_filter';
import { ScheduleDTO } from './schedule';
import { UserDTO as User } from './user';
@ -20,7 +19,6 @@ export interface EscalationPolicyType {
to_time: string | null;
notify_to_schedule: ScheduleDTO['id'] | null;
notify_to_channel: Channel['id'] | null;
custom_button_trigger: ActionDTO['id'] | null;
notify_to_group: UserGroup['id'];
notify_schedule: Schedule['id'];
}
@ -34,6 +32,5 @@ export function prepareEscalationPolicy(value: EscalationPolicyType): Escalation
from_time: null,
to_time: null,
notify_to_schedule: null,
custom_button_trigger: null,
};
}

View file

@ -1,6 +1,6 @@
import { ActionDTO } from 'models/action';
import { Channel } from 'models/channel';
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
import { Schedule } from 'models/schedule/schedule.types';
import { User } from 'models/user/user.types';
import { UserGroup } from 'models/user_group/user_group.types';
@ -17,8 +17,7 @@ export interface EscalationPolicy {
from_time: string | null;
to_time: string | null;
notify_to_channel: Channel['id'] | null;
custom_button_trigger: ActionDTO['id'] | null;
custom_webhook: ActionDTO['id'] | null;
custom_webhook: OutgoingWebhook['id'] | null;
notify_to_group: UserGroup['id'] | null;
notify_schedule: Schedule['id'] | null;
important: boolean | null;

View file

@ -13,13 +13,10 @@ export class OutgoingWebhookStore extends BaseStore {
@observable.shallow
searchResult: { [key: string]: Array<OutgoingWebhook['id']> } = {};
@observable
incidentFilters: any;
constructor(rootStore: RootStore) {
super(rootStore);
this.path = '/custom_buttons/';
this.path = '/webhooks/';
}
@action
@ -46,26 +43,11 @@ export class OutgoingWebhookStore extends BaseStore {
@action
async updateItem(id: OutgoingWebhook['id'], fromOrganization = false) {
let outgoingWebhook;
try {
outgoingWebhook = await this.getById(id, true, fromOrganization);
} catch (error) {
if (error.response.data.error_code === 'wrong_team') {
outgoingWebhook = {
id,
name: '🔒 Private outgoing webhook',
private: true,
};
}
}
if (outgoingWebhook) {
this.items = {
...this.items,
[id]: outgoingWebhook,
};
}
const response = await this.getById(id, false, fromOrganization);
this.items = {
...this.items,
[id]: response,
};
}
@action
@ -95,13 +77,6 @@ export class OutgoingWebhookStore extends BaseStore {
};
}
@action
async updateOutgoingWebhooksFilters(params: any) {
this.incidentFilters = params;
this.updateItems();
}
getSearchResult(query = '') {
if (!this.searchResult[query]) {
return undefined;
@ -109,4 +84,17 @@ export class OutgoingWebhookStore extends BaseStore {
return this.searchResult[query].map((outgoingWebhookId: OutgoingWebhook['id']) => this.items[outgoingWebhookId]);
}
async getLastResponses(id: OutgoingWebhook['id']) {
const result = await makeRequest(`${this.path}${id}/responses`, {});
return result;
}
async renderPreview(id: OutgoingWebhook['id'], template_name: string, template_body: string, payload) {
return await makeRequest(`${this.path}${id}/preview_template/`, {
method: 'POST',
data: { template_name, template_body, payload },
});
}
}

View file

@ -3,11 +3,30 @@ import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
export interface OutgoingWebhook {
authorization_header: string;
data: string;
forward_whole_payload: boolean;
forward_all: boolean;
http_method: string;
id: string;
name: string;
password: string;
team: GrafanaTeam['id'];
user: null;
webhook: string;
trigger_type: number;
trigger_type_name: string;
url: string;
username: null;
headers: string;
trigger_template: string;
last_response_log?: OutgoingWebhookResponse;
is_webhook_enabled: boolean;
is_legacy: boolean;
}
export interface OutgoingWebhookResponse {
timestamp: string;
url: string;
request_trigger: string;
request_headers: string;
request_data: string;
status_code: string;
content: string;
event_data: string;
}

View file

@ -1,110 +0,0 @@
import { action, observable } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
import { RootStore } from 'state';
import { OutgoingWebhook2 } from './outgoing_webhook_2.types';
export class OutgoingWebhook2Store extends BaseStore {
@observable.shallow
items: { [id: string]: OutgoingWebhook2 } = {};
@observable.shallow
searchResult: { [key: string]: Array<OutgoingWebhook2['id']> } = {};
@observable
incidentFilters: any;
constructor(rootStore: RootStore) {
super(rootStore);
this.path = '/webhooks/';
}
@action
async loadItem(id: OutgoingWebhook2['id'], skipErrorHandling = false): Promise<OutgoingWebhook2> {
const outgoingWebhook2 = await this.getById(id, skipErrorHandling);
this.items = {
...this.items,
[id]: outgoingWebhook2,
};
return outgoingWebhook2;
}
@action
async updateById(id: OutgoingWebhook2['id']) {
const response = await this.getById(id);
this.items = {
...this.items,
[id]: response,
};
}
@action
async updateItem(id: OutgoingWebhook2['id'], fromOrganization = false) {
const response = await this.getById(id, false, fromOrganization);
this.items = {
...this.items,
[id]: response,
};
}
@action
async updateItems(query: any = '') {
const params = typeof query === 'string' ? { search: query } : query;
const results = await makeRequest(`${this.path}`, {
params,
});
this.items = {
...this.items,
...results.reduce(
(acc: { [key: number]: OutgoingWebhook2 }, item: OutgoingWebhook2) => ({
...acc,
[item.id]: item,
}),
{}
),
};
const key = typeof query === 'string' ? query : '';
this.searchResult = {
...this.searchResult,
[key]: results.map((item: OutgoingWebhook2) => item.id),
};
}
@action
async updateOutgoingWebhooks2Filters(params: any) {
this.incidentFilters = params;
this.updateItems();
}
getSearchResult(query = '') {
if (!this.searchResult[query]) {
return undefined;
}
return this.searchResult[query].map((outgoingWebhook2Id: OutgoingWebhook2['id']) => this.items[outgoingWebhook2Id]);
}
async getLastResponses(id: OutgoingWebhook2['id']) {
const result = await makeRequest(`${this.path}${id}/responses`, {});
return result;
}
async renderPreview(id: OutgoingWebhook2['id'], template_name: string, template_body: string, payload) {
return await makeRequest(`${this.path}${id}/preview_template/`, {
method: 'POST',
data: { template_name, template_body, payload },
});
}
}

View file

@ -1,32 +0,0 @@
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
export interface OutgoingWebhook2 {
authorization_header: string;
data: string;
forward_all: boolean;
http_method: string;
id: string;
name: string;
password: string;
team: GrafanaTeam['id'];
trigger_type: number;
trigger_type_name: string;
url: string;
username: null;
headers: string;
trigger_template: string;
last_response_log?: OutgoingWebhook2Response;
is_webhook_enabled: boolean;
is_legacy: boolean;
}
export interface OutgoingWebhook2Response {
timestamp: string;
url: string;
request_trigger: string;
request_headers: string;
request_data: string;
status_code: string;
content: string;
event_data: string;
}

View file

@ -1,9 +0,0 @@
.header {
display: flex;
align-items: center;
width: 100%;
}
.filters {
margin-bottom: 20px;
}

View file

@ -1,12 +1,24 @@
import React from 'react';
import { Button, HorizontalGroup } from '@grafana/ui';
import {
Button,
ConfirmModal,
ConfirmModalProps,
HorizontalGroup,
Icon,
IconButton,
VerticalGroup,
WithContextMenu,
} from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import moment from 'moment-timezone';
import LegacyNavHeading from 'navbar/LegacyNavHeading';
import CopyToClipboard from 'react-copy-to-clipboard';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import GTable from 'components/GTable/GTable';
import HamburgerMenu from 'components/HamburgerMenu/HamburgerMenu';
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
import {
getWrongTeamResponseInfo,
@ -14,37 +26,43 @@ import {
} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
import PluginLink from 'components/PluginLink/PluginLink';
import Text from 'components/Text/Text';
import WithConfirm from 'components/WithConfirm/WithConfirm';
import OutgoingWebhookForm from 'containers/OutgoingWebhookForm/OutgoingWebhookForm';
import RemoteFilters from 'containers/RemoteFilters/RemoteFilters';
import TeamName from 'containers/TeamName/TeamName';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { ActionDTO } from 'models/action';
import { FiltersValues } from 'models/filters/filters.types';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import { openErrorNotification, openNotification } from 'utils';
import { isUserActionAllowed, UserActions } from 'utils/authorization';
import { PLUGIN_ROOT } from 'utils/consts';
import styles from './OutgoingWebhooks.module.css';
import styles from './OutgoingWebhooks.module.scss';
import { WebhookFormActionType } from './OutgoingWebhooks.types';
const cx = cn.bind(styles);
interface OutgoingWebhooksProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
interface OutgoingWebhooksProps
extends WithStoreProps,
PageProps,
RouteComponentProps<{ id: string; action: string }> {}
interface OutgoingWebhooksState extends PageBaseState {
outgoingWebhookIdToEdit?: OutgoingWebhook['id'] | 'new';
outgoingWebhookAction?: WebhookFormActionType;
outgoingWebhookId?: OutgoingWebhook['id'];
confirmationModal: ConfirmModalProps;
}
@observer
class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWebhooksState> {
state: OutgoingWebhooksState = {
errorData: initErrorDataState(),
confirmationModal: undefined,
};
componentDidUpdate(prevProps: OutgoingWebhooksProps) {
if (prevProps.match.params.id !== this.props.match.params.id) {
if (prevProps.match.params.id !== this.props.match.params.id && !this.state.outgoingWebhookAction) {
this.parseQueryParams();
}
}
@ -52,56 +70,71 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
parseQueryParams = async () => {
this.setState((_prevState) => ({
errorData: initErrorDataState(),
outgoingWebhookIdToEdit: undefined,
outgoingWebhookId: undefined,
})); // reset state on query parse
const {
store,
match: {
params: { id },
params: { id, action },
},
} = this.props;
if (!id) {
return;
if (action) {
this.setState({ outgoingWebhookId: id, outgoingWebhookAction: convertWebhookUrlToAction(action) });
}
let outgoingWebhook: OutgoingWebhook | void = undefined;
const isNewWebhook = id === 'new';
if (!isNewWebhook) {
outgoingWebhook = await store.outgoingWebhookStore
if (isNewWebhook) {
this.setState({ outgoingWebhookId: id, outgoingWebhookAction: WebhookFormActionType.NEW });
} else if (id) {
await store.outgoingWebhookStore
.loadItem(id, true)
.catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } }));
}
if (outgoingWebhook || isNewWebhook) {
this.setState({ outgoingWebhookIdToEdit: id });
.catch((error) =>
this.setState({ errorData: { ...getWrongTeamResponseInfo(error) }, outgoingWebhookAction: undefined })
);
}
};
update = () => {
const { store } = this.props;
return store.outgoingWebhookStore.updateItems();
};
render() {
const { store, query } = this.props;
const { outgoingWebhookIdToEdit, errorData } = this.state;
const {
store,
history,
match: {
params: { id },
},
} = this.props;
const { outgoingWebhookId, outgoingWebhookAction, errorData, confirmationModal } = this.state;
const webhooks = store.outgoingWebhookStore.getSearchResult();
const columns = [
{
width: '35%',
width: '25%',
title: 'Name',
dataIndex: 'name',
render: this.renderName,
},
{
width: '10%',
title: 'Trigger type',
dataIndex: 'trigger_type_name',
},
{
width: '35%',
title: 'Url',
dataIndex: 'webhook',
title: 'URL',
dataIndex: 'url',
render: this.renderUrl,
},
{
width: '10%',
title: 'Last run',
render: this.renderLastRun,
},
{
width: '15%',
@ -109,7 +142,7 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
render: (item: OutgoingWebhook) => this.renderTeam(item, store.grafanaTeamStore.items),
},
{
width: '15%',
width: '20%',
key: 'action',
render: this.renderActionButtons,
},
@ -120,19 +153,32 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
errorData={errorData}
objectName="outgoing webhook"
pageName="outgoing_webhooks"
itemNotFoundMessage={`Outgoing webhook with id=${query?.id} is not found. Please select outgoing webhook from the list.`}
itemNotFoundMessage={`Outgoing webhook with id=${id} was not found. Please select outgoing webhook from the list.`}
>
{() => (
<>
{confirmationModal && (
<ConfirmModal
{...(confirmationModal as ConfirmModalProps)}
onDismiss={() =>
this.setState({
confirmationModal: undefined,
})
}
/>
)}
<div className={cx('root')}>
{this.renderOutgoingWebhooksFilters()}
<GTable
emptyText={webhooks ? 'No outgoing webhooks found' : 'Loading...'}
title={() => (
<div className={cx('header')}>
<LegacyNavHeading>
<Text.Title level={3}>Outgoing Webhooks</Text.Title>
</LegacyNavHeading>
<div className="header__title">
<LegacyNavHeading>
<Text.Title level={3}>Outgoing Webhooks</Text.Title>
</LegacyNavHeading>
</div>
<div className="u-pull-right">
<PluginLink
query={{ page: 'outgoing_webhooks', id: 'new' }}
@ -140,7 +186,7 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
>
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
<Button variant="primary" icon="plus">
New outgoing webhook
Create
</Button>
</WithPermissionControlTooltip>
</PluginLink>
@ -152,11 +198,19 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
data={webhooks}
/>
</div>
{outgoingWebhookIdToEdit && (
{outgoingWebhookId && outgoingWebhookAction && (
<OutgoingWebhookForm
id={outgoingWebhookIdToEdit}
id={outgoingWebhookId}
action={outgoingWebhookAction}
onUpdate={this.update}
onHide={this.handleOutgoingWebhookFormHide}
onDelete={() => {
this.onDeleteClick(outgoingWebhookId).then(() => {
this.setState({ outgoingWebhookId: undefined, outgoingWebhookAction: undefined });
history.push(`${PLUGIN_ROOT}/outgoing_webhooks`);
});
}}
/>
)}
</>
@ -171,7 +225,7 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
<div className={cx('filters')}>
<RemoteFilters
query={query}
page="outgoing_webhooks"
page="webhooks"
grafanaTeamStore={store.grafanaTeamStore}
onChange={this.handleFiltersChange}
/>
@ -195,50 +249,198 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
return <TeamName team={teams[record.team]} />;
}
renderActionButtons = (record: ActionDTO) => {
renderActionButtons = (record: OutgoingWebhook) => {
return (
<HorizontalGroup justify="flex-end">
<WithPermissionControlTooltip key={'edit_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<Button onClick={this.getEditClickHandler(record.id)} fill="text">
Edit
</Button>
</WithPermissionControlTooltip>
<WithPermissionControlTooltip key={'delete_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<WithConfirm>
<Button onClick={this.getDeleteClickHandler(record.id)} fill="text" variant="destructive">
Delete
</Button>
</WithConfirm>
</WithPermissionControlTooltip>
</HorizontalGroup>
<WithContextMenu
renderMenuItems={() => (
<div className={cx('hamburgerMenu')}>
<div className={cx('hamburgerMenu__item')} onClick={() => this.onLastRunClick(record.id)}>
<WithPermissionControlTooltip key={'status_action'} userAction={UserActions.OutgoingWebhooksRead}>
<Text type="primary">View Last Run</Text>
</WithPermissionControlTooltip>
</div>
<div className={cx('hamburgerMenu__item')} onClick={() => this.onEditClick(record.id)}>
<WithPermissionControlTooltip key={'edit_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<Text type="primary">Edit settings</Text>
</WithPermissionControlTooltip>
</div>
<div
className={cx('hamburgerMenu__item')}
onClick={() =>
this.setState({
confirmationModal: {
isOpen: true,
confirmText: 'Confirm',
dismissText: 'Cancel',
onConfirm: () => this.onDisableWebhook(record.id, !record.is_webhook_enabled),
title: `Are you sure you want to ${record.is_webhook_enabled ? 'disable' : 'enable'} webhook?`,
} as ConfirmModalProps,
})
}
>
<WithPermissionControlTooltip key={'disable_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<Text type="primary">{record.is_webhook_enabled ? 'Disable' : 'Enable'}</Text>
</WithPermissionControlTooltip>
</div>
<div className={cx('hamburgerMenu__item')} onClick={() => this.onCopyClick(record.id)}>
<WithPermissionControlTooltip key={'copy_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<Text type="primary">Make a copy</Text>
</WithPermissionControlTooltip>
</div>
<CopyToClipboard text={record.id} onCopy={() => openNotification('Webhook ID has been copied')}>
<div className={cx('hamburgerMenu__item')}>
<HorizontalGroup type="primary" spacing="xs">
<Icon name="clipboard-alt" />
<Text type="primary">UID: {record.id}</Text>
</HorizontalGroup>
</div>
</CopyToClipboard>
<div className={cx('thin-line-break')} />
<div
className={cx('hamburgerMenu__item')}
onClick={() =>
this.setState({
confirmationModal: {
isOpen: true,
confirmText: 'Confirm',
dismissText: 'Cancel',
onConfirm: () => this.onDeleteClick(record.id),
body: 'The action cannot be undone.',
title: `Are you sure you want to delete webhook?`,
} as Partial<ConfirmModalProps> as ConfirmModalProps,
})
}
>
<WithPermissionControlTooltip key={'delete_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<HorizontalGroup spacing="xs">
<IconButton tooltip="Remove" tooltipPlacement="top" variant="destructive" name="trash-alt" />
<Text type="danger">Delete Webhook</Text>
</HorizontalGroup>
</WithPermissionControlTooltip>
</div>
</div>
)}
>
{({ openMenu }) => <HamburgerMenu openMenu={openMenu} listBorder={2} listWidth={225} withBackground />}
</WithContextMenu>
);
};
getDeleteClickHandler = (id: OutgoingWebhook['id']) => {
renderName(name: String) {
return (
<div className="u-break-word">
<span>{name}</span>
</div>
);
}
renderUrl(url: string) {
return (
<div className="u-break-word">
<span>{url}</span>
</div>
);
}
renderLastRun(record: OutgoingWebhook) {
const lastRunMoment = moment(record.last_response_log?.timestamp);
return !record.is_webhook_enabled ? (
<Text type="secondary">Disabled</Text>
) : (
<VerticalGroup spacing="none">
<Text type="secondary">{lastRunMoment.isValid() ? lastRunMoment.format('MMM DD, YYYY') : '-'}</Text>
<Text type="secondary">{lastRunMoment.isValid() ? lastRunMoment.format('HH:mm') : ''}</Text>
<Text type="secondary">
{lastRunMoment.isValid()
? record.last_response_log?.status_code
? 'Status: ' + record.last_response_log?.status_code
: 'Check Status'
: ''}
</Text>
</VerticalGroup>
);
}
onDeleteClick = (id: OutgoingWebhook['id']): Promise<void> => {
const { store } = this.props;
return () => {
store.alertReceiveChannelStore.deleteCustomButton(id).then(this.update);
};
return store.outgoingWebhookStore
.delete(id)
.then(this.update)
.then(() => openNotification('Webhook has been removed'))
.catch(() => openNotification('Webook could not been removed'))
.finally(() => this.setState({ confirmationModal: undefined }));
};
getEditClickHandler = (id: OutgoingWebhook['id']) => {
onEditClick = (id: OutgoingWebhook['id']) => {
const { history } = this.props;
return () => {
this.setState({ outgoingWebhookIdToEdit: id });
this.setState({ outgoingWebhookId: id, outgoingWebhookAction: WebhookFormActionType.EDIT_SETTINGS }, () =>
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`)
);
};
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/${id}`);
onCopyClick = (id: OutgoingWebhook['id']) => {
const { history } = this.props;
this.setState({ outgoingWebhookId: id, outgoingWebhookAction: WebhookFormActionType.COPY }, () =>
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/copy/${id}`)
);
};
onDisableWebhook = (id: OutgoingWebhook['id'], isEnabled: boolean) => {
const {
store: { outgoingWebhookStore },
} = this.props;
const data = {
...{ ...outgoingWebhookStore.items[id], is_webhook_enabled: isEnabled },
is_legacy: false,
};
outgoingWebhookStore
.update(id, data)
.then(() => this.update())
.then(() => openNotification(`Webhook has been ${isEnabled ? 'enabled' : 'disabled'}`))
.catch(() => openErrorNotification('Webhook could not been updated'))
.finally(() => this.setState({ confirmationModal: undefined }));
};
onLastRunClick = (id: OutgoingWebhook['id']) => {
const { history } = this.props;
this.setState({ outgoingWebhookId: id, outgoingWebhookAction: WebhookFormActionType.VIEW_LAST_RUN }, () =>
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`)
);
};
handleOutgoingWebhookFormHide = () => {
const { history } = this.props;
this.setState({ outgoingWebhookIdToEdit: undefined });
this.setState({ outgoingWebhookId: undefined, outgoingWebhookAction: undefined });
history.push(`${PLUGIN_ROOT}/outgoing_webhooks`);
};
}
function convertWebhookUrlToAction(urlAction: string) {
if (urlAction === 'new') {
return WebhookFormActionType.NEW;
} else if (urlAction === 'copy') {
return WebhookFormActionType.COPY;
} else if (urlAction === 'edit') {
return WebhookFormActionType.EDIT_SETTINGS;
} else {
return WebhookFormActionType.VIEW_LAST_RUN;
}
}
export { OutgoingWebhooks };
export default withRouter(withMobXProviderContext(OutgoingWebhooks));

View file

@ -1,450 +0,0 @@
import React from 'react';
import {
Button,
ConfirmModal,
ConfirmModalProps,
HorizontalGroup,
Icon,
IconButton,
VerticalGroup,
WithContextMenu,
} from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import moment from 'moment-timezone';
import LegacyNavHeading from 'navbar/LegacyNavHeading';
import CopyToClipboard from 'react-copy-to-clipboard';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import GTable from 'components/GTable/GTable';
import HamburgerMenu from 'components/HamburgerMenu/HamburgerMenu';
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
import {
getWrongTeamResponseInfo,
initErrorDataState,
} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
import PluginLink from 'components/PluginLink/PluginLink';
import Text from 'components/Text/Text';
import OutgoingWebhook2Form from 'containers/OutgoingWebhook2Form/OutgoingWebhook2Form';
import RemoteFilters from 'containers/RemoteFilters/RemoteFilters';
import TeamName from 'containers/TeamName/TeamName';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { FiltersValues } from 'models/filters/filters.types';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
import { OutgoingWebhook2 } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
import { AppFeature } from 'state/features';
import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import { openErrorNotification, openNotification } from 'utils';
import { isUserActionAllowed, UserActions } from 'utils/authorization';
import { PLUGIN_ROOT } from 'utils/consts';
import styles from './OutgoingWebhooks2.module.scss';
import { WebhookFormActionType } from './OutgoingWebhooks2.types';
const cx = cn.bind(styles);
interface OutgoingWebhooks2Props
extends WithStoreProps,
PageProps,
RouteComponentProps<{ id: string; action: string }> {}
interface OutgoingWebhooks2State extends PageBaseState {
outgoingWebhook2Action?: WebhookFormActionType;
outgoingWebhook2Id?: OutgoingWebhook2['id'];
confirmationModal: ConfirmModalProps;
}
@observer
class OutgoingWebhooks2 extends React.Component<OutgoingWebhooks2Props, OutgoingWebhooks2State> {
state: OutgoingWebhooks2State = {
errorData: initErrorDataState(),
confirmationModal: undefined,
};
componentDidUpdate(prevProps: OutgoingWebhooks2Props) {
if (prevProps.match.params.id !== this.props.match.params.id && !this.state.outgoingWebhook2Action) {
this.parseQueryParams();
}
}
parseQueryParams = async () => {
this.setState((_prevState) => ({
errorData: initErrorDataState(),
outgoingWebhook2Id: undefined,
})); // reset state on query parse
const {
store,
match: {
params: { id, action },
},
} = this.props;
if (action) {
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: convertWebhookUrlToAction(action) });
}
const isNewWebhook = id === 'new';
if (isNewWebhook) {
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: WebhookFormActionType.NEW });
} else if (id) {
await store.outgoingWebhook2Store
.loadItem(id, true)
.catch((error) =>
this.setState({ errorData: { ...getWrongTeamResponseInfo(error) }, outgoingWebhook2Action: undefined })
);
}
};
update = () => {
const { store } = this.props;
return store.outgoingWebhook2Store.updateItems();
};
render() {
const {
store,
history,
match: {
params: { id },
},
} = this.props;
const { outgoingWebhook2Id, outgoingWebhook2Action, errorData, confirmationModal } = this.state;
const webhooks = store.outgoingWebhook2Store.getSearchResult();
const columns = [
{
width: '25%',
title: 'Name',
dataIndex: 'name',
render: this.renderName,
},
{
width: '10%',
title: 'Trigger type',
dataIndex: 'trigger_type_name',
},
{
width: '35%',
title: 'URL',
dataIndex: 'url',
render: this.renderUrl,
},
{
width: '10%',
title: 'Last run',
render: this.renderLastRun,
},
{
width: '15%',
title: 'Team',
render: (item: OutgoingWebhook) => this.renderTeam(item, store.grafanaTeamStore.items),
},
{
width: '20%',
key: 'action',
render: this.renderActionButtons,
},
];
return store.hasFeature(AppFeature.Webhooks2) ? (
<PageErrorHandlingWrapper
errorData={errorData}
objectName="outgoing webhook 2"
pageName="outgoing_webhooks_2"
itemNotFoundMessage={`Outgoing webhook with id=${id} was not found. Please select outgoing webhook from the list.`}
>
{() => (
<>
{confirmationModal && (
<ConfirmModal
{...(confirmationModal as ConfirmModalProps)}
onDismiss={() =>
this.setState({
confirmationModal: undefined,
})
}
/>
)}
<div className={cx('root')}>
{this.renderOutgoingWebhooksFilters()}
<GTable
emptyText={webhooks ? 'No outgoing webhooks found' : 'Loading...'}
title={() => (
<div className={cx('header')}>
<div className="header__title">
<LegacyNavHeading>
<Text.Title level={3}>Outgoing Webhooks</Text.Title>
</LegacyNavHeading>
</div>
<div className="u-pull-right">
<PluginLink
query={{ page: 'outgoing_webhooks', id: 'new' }}
disabled={!isUserActionAllowed(UserActions.OutgoingWebhooksWrite)}
>
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
<Button variant="primary" icon="plus">
Create
</Button>
</WithPermissionControlTooltip>
</PluginLink>
</div>
</div>
)}
rowKey="id"
columns={columns}
data={webhooks}
/>
</div>
{outgoingWebhook2Id && outgoingWebhook2Action && (
<OutgoingWebhook2Form
id={outgoingWebhook2Id}
action={outgoingWebhook2Action}
onUpdate={this.update}
onHide={this.handleOutgoingWebhookFormHide}
onDelete={() => {
this.onDeleteClick(outgoingWebhook2Id).then(() => {
this.setState({ outgoingWebhook2Id: undefined, outgoingWebhook2Action: undefined });
history.push(`${PLUGIN_ROOT}/outgoing_webhooks`);
});
}}
/>
)}
</>
)}
</PageErrorHandlingWrapper>
) : (
<Text>Outgoing webhooks 2 functionality is not enabled.</Text>
);
}
renderOutgoingWebhooksFilters() {
const { query, store } = this.props;
return (
<div className={cx('filters')}>
<RemoteFilters
query={query}
page="webhooks"
grafanaTeamStore={store.grafanaTeamStore}
onChange={this.handleFiltersChange}
/>
</div>
);
}
handleFiltersChange = (filters: FiltersValues, isOnMount) => {
const { store } = this.props;
const { outgoingWebhook2Store } = store;
outgoingWebhook2Store.updateItems(filters).then(() => {
if (isOnMount) {
this.parseQueryParams();
}
});
};
renderTeam(record: OutgoingWebhook, teams: any) {
return <TeamName team={teams[record.team]} />;
}
renderActionButtons = (record: OutgoingWebhook2) => {
return (
<WithContextMenu
renderMenuItems={() => (
<div className={cx('hamburgerMenu')}>
<div className={cx('hamburgerMenu__item')} onClick={() => this.onLastRunClick(record.id)}>
<WithPermissionControlTooltip key={'status_action'} userAction={UserActions.OutgoingWebhooksRead}>
<Text type="primary">View Last Run</Text>
</WithPermissionControlTooltip>
</div>
<div className={cx('hamburgerMenu__item')} onClick={() => this.onEditClick(record.id)}>
<WithPermissionControlTooltip key={'edit_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<Text type="primary">Edit settings</Text>
</WithPermissionControlTooltip>
</div>
<div
className={cx('hamburgerMenu__item')}
onClick={() =>
this.setState({
confirmationModal: {
isOpen: true,
confirmText: 'Confirm',
dismissText: 'Cancel',
onConfirm: () => this.onDisableWebhook(record.id, !record.is_webhook_enabled),
title: `Are you sure you want to ${record.is_webhook_enabled ? 'disable' : 'enable'} webhook?`,
} as ConfirmModalProps,
})
}
>
<WithPermissionControlTooltip key={'disable_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<Text type="primary">{record.is_webhook_enabled ? 'Disable' : 'Enable'}</Text>
</WithPermissionControlTooltip>
</div>
<div className={cx('hamburgerMenu__item')} onClick={() => this.onCopyClick(record.id)}>
<WithPermissionControlTooltip key={'copy_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<Text type="primary">Make a copy</Text>
</WithPermissionControlTooltip>
</div>
<CopyToClipboard text={record.id} onCopy={() => openNotification('Webhook ID has been copied')}>
<div className={cx('hamburgerMenu__item')}>
<HorizontalGroup type="primary" spacing="xs">
<Icon name="clipboard-alt" />
<Text type="primary">UID: {record.id}</Text>
</HorizontalGroup>
</div>
</CopyToClipboard>
<div className={cx('thin-line-break')} />
<div
className={cx('hamburgerMenu__item')}
onClick={() =>
this.setState({
confirmationModal: {
isOpen: true,
confirmText: 'Confirm',
dismissText: 'Cancel',
onConfirm: () => this.onDeleteClick(record.id),
body: 'The action cannot be undone.',
title: `Are you sure you want to delete webhook?`,
} as Partial<ConfirmModalProps> as ConfirmModalProps,
})
}
>
<WithPermissionControlTooltip key={'delete_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<HorizontalGroup spacing="xs">
<IconButton tooltip="Remove" tooltipPlacement="top" variant="destructive" name="trash-alt" />
<Text type="danger">Delete Webhook</Text>
</HorizontalGroup>
</WithPermissionControlTooltip>
</div>
</div>
)}
>
{({ openMenu }) => <HamburgerMenu openMenu={openMenu} listBorder={2} listWidth={225} withBackground />}
</WithContextMenu>
);
};
renderName(name: String) {
return (
<div className="u-break-word">
<span>{name}</span>
</div>
);
}
renderUrl(url: string) {
return (
<div className="u-break-word">
<span>{url}</span>
</div>
);
}
renderLastRun(record: OutgoingWebhook2) {
const lastRunMoment = moment(record.last_response_log?.timestamp);
return !record.is_webhook_enabled ? (
<Text type="secondary">Disabled</Text>
) : (
<VerticalGroup spacing="none">
<Text type="secondary">{lastRunMoment.isValid() ? lastRunMoment.format('MMM DD, YYYY') : '-'}</Text>
<Text type="secondary">{lastRunMoment.isValid() ? lastRunMoment.format('HH:mm') : ''}</Text>
<Text type="secondary">
{lastRunMoment.isValid()
? record.last_response_log?.status_code
? 'Status: ' + record.last_response_log?.status_code
: 'Check Status'
: ''}
</Text>
</VerticalGroup>
);
}
onDeleteClick = (id: OutgoingWebhook2['id']): Promise<void> => {
const { store } = this.props;
return store.outgoingWebhook2Store
.delete(id)
.then(this.update)
.then(() => openNotification('Webhook has been removed'))
.catch(() => openNotification('Webook could not been removed'))
.finally(() => this.setState({ confirmationModal: undefined }));
};
onEditClick = (id: OutgoingWebhook2['id']) => {
const { history } = this.props;
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: WebhookFormActionType.EDIT_SETTINGS }, () =>
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`)
);
};
onCopyClick = (id: OutgoingWebhook2['id']) => {
const { history } = this.props;
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: WebhookFormActionType.COPY }, () =>
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/copy/${id}`)
);
};
onDisableWebhook = (id: OutgoingWebhook2['id'], isEnabled: boolean) => {
const {
store: { outgoingWebhook2Store },
} = this.props;
const data = {
...{ ...outgoingWebhook2Store.items[id], is_webhook_enabled: isEnabled },
is_legacy: false,
};
outgoingWebhook2Store
.update(id, data)
.then(() => this.update())
.then(() => openNotification(`Webhook has been ${isEnabled ? 'enabled' : 'disabled'}`))
.catch(() => openErrorNotification('Webhook could not been updated'))
.finally(() => this.setState({ confirmationModal: undefined }));
};
onLastRunClick = (id: OutgoingWebhook2['id']) => {
const { history } = this.props;
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: WebhookFormActionType.VIEW_LAST_RUN }, () =>
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`)
);
};
handleOutgoingWebhookFormHide = () => {
const { history } = this.props;
this.setState({ outgoingWebhook2Id: undefined, outgoingWebhook2Action: undefined });
history.push(`${PLUGIN_ROOT}/outgoing_webhooks`);
};
}
function convertWebhookUrlToAction(urlAction: string) {
if (urlAction === 'new') {
return WebhookFormActionType.NEW;
} else if (urlAction === 'copy') {
return WebhookFormActionType.COPY;
} else if (urlAction === 'edit') {
return WebhookFormActionType.EDIT_SETTINGS;
} else {
return WebhookFormActionType.VIEW_LAST_RUN;
}
}
export { OutgoingWebhooks2 };
export default withRouter(withMobXProviderContext(OutgoingWebhooks2));

View file

@ -28,7 +28,6 @@ import Integration from 'pages/integration/Integration';
import Integrations from 'pages/integrations/Integrations';
import Maintenance from 'pages/maintenance/Maintenance';
import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks';
import OutgoingWebhooks2 from 'pages/outgoing_webhooks_2/OutgoingWebhooks2';
import Schedule from 'pages/schedule/Schedule';
import Schedules from 'pages/schedules/Schedules';
import SettingsPage from 'pages/settings/SettingsPage';
@ -37,7 +36,6 @@ import CloudPage from 'pages/settings/tabs/Cloud/CloudPage';
import LiveSettings from 'pages/settings/tabs/LiveSettings/LiveSettingsPage';
import Users from 'pages/users/Users';
import { rootStore } from 'state';
import { AppFeature } from 'state/features';
import { useStore } from 'state/useStore';
import { isUserActionAllowed } from 'utils/authorization';
@ -154,11 +152,7 @@ export const Root = observer((props: AppRootProps) => {
<Schedule query={query} basicDataLoaded={basicDataLoaded} />
</Route>
<Route path={getRoutesForPage('outgoing_webhooks')} exact>
{rootStore.hasFeature(AppFeature.Webhooks2) ? (
<OutgoingWebhooks2 query={query} />
) : (
<OutgoingWebhooks query={query} />
)}
<OutgoingWebhooks query={query} />
</Route>
<Route path={getRoutesForPage('maintenance')} exact>
<Maintenance />

View file

@ -5,5 +5,4 @@ export enum AppFeature {
CloudNotifications = 'grafana_cloud_notifications',
CloudConnection = 'grafana_cloud_connection',
WebSchedules = 'web_schedules',
Webhooks2 = 'webhooks2',
}

View file

@ -21,7 +21,6 @@ import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
import { HeartbeatStore } from 'models/heartbeat/heartbeat';
import { OrganizationStore } from 'models/organization/organization';
import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook';
import { OutgoingWebhook2Store } from 'models/outgoing_webhook_2/outgoing_webhook_2';
import { ResolutionNotesStore } from 'models/resolution_note/resolution_note';
import { ScheduleStore } from 'models/schedule/schedule';
import { SlackStore } from 'models/slack/slack';
@ -84,15 +83,12 @@ export class RootBaseStore {
onCallApiUrl: string;
// --------------------------
userStore = new UserStore(this);
cloudStore = new CloudStore(this);
directPagingStore = new DirectPagingStore(this);
grafanaTeamStore = new GrafanaTeamStore(this);
alertReceiveChannelStore = new AlertReceiveChannelStore(this);
outgoingWebhookStore = new OutgoingWebhookStore(this);
outgoingWebhook2Store = new OutgoingWebhook2Store(this);
alertReceiveChannelFiltersStore = new AlertReceiveChannelFiltersStore(this);
escalationChainStore = new EscalationChainStore(this);
escalationPolicyStore = new EscalationPolicyStore(this);
@ -108,6 +104,7 @@ export class RootBaseStore {
apiTokenStore = new ApiTokenStore(this);
globalSettingStore = new GlobalSettingStore(this);
filtersStore = new FiltersStore(this);
// stores
async updateBasicData() {