diff --git a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx index 89969613..a3e43393 100644 --- a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx +++ b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx @@ -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 - { - const team = teamStore.items[outgoingWebhookStore.items[item.value].team]; - return ( - <> - {item.label} - - - ); - }} - width={'auto'} - /> - - ); - } - 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 { - const team = teamStore.items[outgoingWebhook2Store.items[item.value].team]; + const team = teamStore.items[outgoingWebhookStore.items[item.value].team]; return ( <> {item.label} @@ -443,7 +408,7 @@ export class EscalationPolicy extends React.Component { - const webhook = outgoingWebhook2Store.items[id]; + const webhook = outgoingWebhookStore.items[id]; return webhook.trigger_type_name === 'Escalation step'; }} /> diff --git a/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx b/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx index b8af2abc..09e58c6a 100644 --- a/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx +++ b/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx @@ -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) = - {store.hasFeature(AppFeature.Webhooks2) && ( - - - ID {alertReceiveChannel.id} -
- (click to copy ID to clipboard) - - } - tooltipPlacement="top" - name="info-circle" - /> -
- )} + + + ID {alertReceiveChannel.id} +
+ (click to copy ID to clipboard) + + } + tooltipPlacement="top" + name="info-circle" + /> +
{alertReceiveChannelCounter && ( { teamStore={store.grafanaTeamStore} scheduleStore={store.scheduleStore} outgoingWebhookStore={store.outgoingWebhookStore} - outgoingWebhook2Store={store.outgoingWebhook2Store} isDisabled={isDisabled} /> ); diff --git a/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.module.css b/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.module.css deleted file mode 100644 index a4613c64..00000000 --- a/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.module.css +++ /dev/null @@ -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; -} diff --git a/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.tsx b/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.tsx deleted file mode 100644 index 42e3bac6..00000000 --- a/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.tsx +++ /dev/null @@ -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( - 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) => { - (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 ( -
-
{renderedControl}
-
- ); - } - - 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 = ; - - if (action === WebhookFormActionType.NEW || action === WebhookFormActionType.COPY) { - // show just the creation form, not the tabs - return ( - <> - -
{renderWebhookForm()}
-
- {templateToEdit && ( - { - onFormChangeFn?.fn(value); - setTemplateToEdit(undefined); - }} - onHide={() => setTemplateToEdit(undefined)} - template={templateToEdit} - /> - )} - - ); - } - - return ( - // show tabbed drawer (edit/live_run) - <> - -
- - { - setActiveTab(WebhookTabs.Settings.key); - history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`); - }} - active={activeTab === WebhookTabs.Settings.key} - label={WebhookTabs.Settings.value} - /> - - { - setActiveTab(WebhookTabs.LastRun.key); - history.push(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`); - }} - active={activeTab === WebhookTabs.LastRun.key} - label={WebhookTabs.LastRun.value} - /> - - - -
-
- {templateToEdit && ( - { - onFormChangeFn?.fn(value); - setTemplateToEdit(undefined); - }} - onHide={() => setTemplateToEdit(undefined)} - template={templateToEdit} - /> - )} - - ); - - function renderWebhookForm() { - return ( - <> -
- -
- - - - - - -
-
- - ); - } -}); - -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) => void; - formElement: React.ReactElement; -} - -const WebhookTabsContent: React.FC = ({ - id, - action, - activeTab, - data, - onHide, - onUpdate, - onDelete, - formElement, -}) => { - const [confirmationModal, setConfirmationModal] = useState(undefined); - - return ( -
- {confirmationModal && ( - setConfirmationModal(undefined)} /> - )} - - {activeTab === WebhookTabs.Settings.key && ( - <> -
- {formElement} -
- - - - - - - - - -
-
- {data.is_legacy ? ( -
- Legacy migrated webhooks are not editable. Make a copy to make changes. -
- ) : ( - '' - )} - - )} - {activeTab === WebhookTabs.LastRun.key && } -
- ); -}; - -export default OutgoingWebhook2Form; diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.ts b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.ts deleted file mode 100644 index b4356017..00000000 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.ts +++ /dev/null @@ -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", - }, - ], -}; diff --git a/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.config.tsx b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.tsx similarity index 99% rename from grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.config.tsx rename to grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.tsx index d96588e1..b67a3818 100644 --- a/grafana-plugin/src/containers/OutgoingWebhook2Form/OutgoingWebhook2Form.config.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.config.tsx @@ -18,7 +18,7 @@ export const WebhookTriggerType = { }; export const form: { name: string; fields: FormItem[] } = { - name: 'OutgoingWebhook2', + name: 'OutgoingWebhook', fields: [ { name: 'name', diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css index b0cae583..a4613c64 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css @@ -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; +} diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx index 74c8e94f..a006e5d1 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx @@ -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( + 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) => { - (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 ( +
+
{renderedControl}
+
+ ); + } + + 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 = ; + + if (action === WebhookFormActionType.NEW || action === WebhookFormActionType.COPY) { + // show just the creation form, not the tabs + return ( + <> + +
{renderWebhookForm()}
+
+ {templateToEdit && ( + { + onFormChangeFn?.fn(value); + setTemplateToEdit(undefined); + }} + onHide={() => setTemplateToEdit(undefined)} + template={templateToEdit} + /> + )} + + ); + } + return ( - -
- - - - - - - -
-
+ // show tabbed drawer (edit/live_run) + <> + +
+ + { + setActiveTab(WebhookTabs.Settings.key); + history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`); + }} + active={activeTab === WebhookTabs.Settings.key} + label={WebhookTabs.Settings.value} + /> + + { + setActiveTab(WebhookTabs.LastRun.key); + history.push(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`); + }} + active={activeTab === WebhookTabs.LastRun.key} + label={WebhookTabs.LastRun.value} + /> + + + +
+
+ {templateToEdit && ( + { + onFormChangeFn?.fn(value); + setTemplateToEdit(undefined); + }} + onHide={() => setTemplateToEdit(undefined)} + template={templateToEdit} + /> + )} + ); + + function renderWebhookForm() { + return ( + <> +
+ +
+ + + + + + +
+
+ + ); + } }); +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) => void; + formElement: React.ReactElement; +} + +const WebhookTabsContent: React.FC = ({ + id, + action, + activeTab, + data, + onHide, + onUpdate, + onDelete, + formElement, +}) => { + const [confirmationModal, setConfirmationModal] = useState(undefined); + + return ( +
+ {confirmationModal && ( + setConfirmationModal(undefined)} /> + )} + + {activeTab === WebhookTabs.Settings.key && ( + <> +
+ {formElement} +
+ + + + + + + + + +
+
+ {data.is_legacy ? ( +
+ Legacy migrated webhooks are not editable. Make a copy to make changes. +
+ ) : ( + '' + )} + + )} + {activeTab === WebhookTabs.LastRun.key && } +
+ ); +}; + export default OutgoingWebhookForm; diff --git a/grafana-plugin/src/containers/OutgoingWebhook2Status/OutgoingWebhook2Status.tsx b/grafana-plugin/src/containers/OutgoingWebhookStatus/OutgoingWebhookStatus.tsx similarity index 88% rename from grafana-plugin/src/containers/OutgoingWebhook2Status/OutgoingWebhook2Status.tsx rename to grafana-plugin/src/containers/OutgoingWebhookStatus/OutgoingWebhookStatus.tsx index 4c122f51..c6ea8841 100644 --- a/grafana-plugin/src/containers/OutgoingWebhook2Status/OutgoingWebhook2Status.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookStatus/OutgoingWebhookStatus.tsx @@ -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 (
@@ -119,4 +119,4 @@ const OutgoingWebhook2Status = observer((props: OutgoingWebhook2StatusProps) => ); }); -export default OutgoingWebhook2Status; +export default OutgoingWebhookStatus; diff --git a/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx b/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx index 35bbb032..2f0d4d78 100644 --- a/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx +++ b/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx @@ -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({}); 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) diff --git a/grafana-plugin/src/containers/TemplateResult/TemplateResult.tsx b/grafana-plugin/src/containers/TemplateResult/TemplateResult.tsx index 3a11defb..0732f1cb 100644 --- a/grafana-plugin/src/containers/TemplateResult/TemplateResult.tsx +++ b/grafana-plugin/src/containers/TemplateResult/TemplateResult.tsx @@ -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; diff --git a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx index 0452f94c..8c49e1dd 100644 --- a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx +++ b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx @@ -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(undefined); + useState(undefined); const [selectedTitle, setSelectedTitle] = useState(undefined); const [selectedPayload, setSelectedPayload] = useState(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)); diff --git a/grafana-plugin/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx b/grafana-plugin/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx index 63dd8e5c..aeb559df 100644 --- a/grafana-plugin/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx +++ b/grafana-plugin/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx @@ -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; } diff --git a/grafana-plugin/src/models/action.ts b/grafana-plugin/src/models/action.ts deleted file mode 100644 index ea6fc747..00000000 --- a/grafana-plugin/src/models/action.ts +++ /dev/null @@ -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; -} diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index 9350715b..f0764c7d 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -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}/`, {}); diff --git a/grafana-plugin/src/models/escalation_policy.ts b/grafana-plugin/src/models/escalation_policy.ts index a8f501a3..286178c6 100644 --- a/grafana-plugin/src/models/escalation_policy.ts +++ b/grafana-plugin/src/models/escalation_policy.ts @@ -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, }; } diff --git a/grafana-plugin/src/models/escalation_policy/escalation_policy.types.ts b/grafana-plugin/src/models/escalation_policy/escalation_policy.types.ts index 5e757d5b..a4918d0f 100644 --- a/grafana-plugin/src/models/escalation_policy/escalation_policy.types.ts +++ b/grafana-plugin/src/models/escalation_policy/escalation_policy.types.ts @@ -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; diff --git a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts index 50d5b069..7668d1e1 100644 --- a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts +++ b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts @@ -13,13 +13,10 @@ export class OutgoingWebhookStore extends BaseStore { @observable.shallow searchResult: { [key: string]: Array } = {}; - @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 }, + }); + } } diff --git a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts index abafc343..e3d98c9a 100644 --- a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts +++ b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts @@ -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; } diff --git a/grafana-plugin/src/models/outgoing_webhook_2/outgoing_webhook_2.ts b/grafana-plugin/src/models/outgoing_webhook_2/outgoing_webhook_2.ts deleted file mode 100644 index 5f8ff367..00000000 --- a/grafana-plugin/src/models/outgoing_webhook_2/outgoing_webhook_2.ts +++ /dev/null @@ -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 } = {}; - - @observable - incidentFilters: any; - - constructor(rootStore: RootStore) { - super(rootStore); - - this.path = '/webhooks/'; - } - - @action - async loadItem(id: OutgoingWebhook2['id'], skipErrorHandling = false): Promise { - 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 }, - }); - } -} diff --git a/grafana-plugin/src/models/outgoing_webhook_2/outgoing_webhook_2.types.ts b/grafana-plugin/src/models/outgoing_webhook_2/outgoing_webhook_2.types.ts deleted file mode 100644 index 5035e930..00000000 --- a/grafana-plugin/src/models/outgoing_webhook_2/outgoing_webhook_2.types.ts +++ /dev/null @@ -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; -} diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.module.css b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.module.css deleted file mode 100644 index ed38b9a7..00000000 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.header { - display: flex; - align-items: center; - width: 100%; -} - -.filters { - margin-bottom: 20px; -} diff --git a/grafana-plugin/src/pages/outgoing_webhooks_2/OutgoingWebhooks2.module.scss b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.module.scss similarity index 100% rename from grafana-plugin/src/pages/outgoing_webhooks_2/OutgoingWebhooks2.module.scss rename to grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.module.scss diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 4ffe98a7..899aeecd 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -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 { 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 { 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 this.renderTeam(item, store.grafanaTeamStore.items), }, { - width: '15%', + width: '20%', key: 'action', render: this.renderActionButtons, }, @@ -120,19 +153,32 @@ class OutgoingWebhooks extends React.Component {() => ( <> + {confirmationModal && ( + + this.setState({ + confirmationModal: undefined, + }) + } + /> + )} +
{this.renderOutgoingWebhooksFilters()} (
- - Outgoing Webhooks - +
+ + Outgoing Webhooks + +
@@ -152,11 +198,19 @@ class OutgoingWebhooks extends React.Component
- {outgoingWebhookIdToEdit && ( + + {outgoingWebhookId && outgoingWebhookAction && ( { + 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 @@ -195,50 +249,198 @@ class OutgoingWebhooks extends React.Component; } - renderActionButtons = (record: ActionDTO) => { + renderActionButtons = (record: OutgoingWebhook) => { return ( - - - - - - - - - - + ( +
+
this.onLastRunClick(record.id)}> + + View Last Run + +
+ +
this.onEditClick(record.id)}> + + Edit settings + +
+ +
+ 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, + }) + } + > + + {record.is_webhook_enabled ? 'Disable' : 'Enable'} + +
+ +
this.onCopyClick(record.id)}> + + Make a copy + +
+ + openNotification('Webhook ID has been copied')}> +
+ + + UID: {record.id} + +
+
+ +
+ +
+ 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 as ConfirmModalProps, + }) + } + > + + + + Delete Webhook + + +
+
+ )} + > + {({ openMenu }) => } + ); }; - getDeleteClickHandler = (id: OutgoingWebhook['id']) => { + renderName(name: String) { + return ( +
+ {name} +
+ ); + } + + renderUrl(url: string) { + return ( +
+ {url} +
+ ); + } + + renderLastRun(record: OutgoingWebhook) { + const lastRunMoment = moment(record.last_response_log?.timestamp); + + return !record.is_webhook_enabled ? ( + Disabled + ) : ( + + {lastRunMoment.isValid() ? lastRunMoment.format('MMM DD, YYYY') : '-'} + {lastRunMoment.isValid() ? lastRunMoment.format('HH:mm') : ''} + + {lastRunMoment.isValid() + ? record.last_response_log?.status_code + ? 'Status: ' + record.last_response_log?.status_code + : 'Check Status' + : ''} + + + ); + } + + onDeleteClick = (id: OutgoingWebhook['id']): Promise => { 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)); diff --git a/grafana-plugin/src/pages/outgoing_webhooks_2/OutgoingWebhooks2.types.ts b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.types.ts similarity index 100% rename from grafana-plugin/src/pages/outgoing_webhooks_2/OutgoingWebhooks2.types.ts rename to grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.types.ts diff --git a/grafana-plugin/src/pages/outgoing_webhooks_2/OutgoingWebhooks2.tsx b/grafana-plugin/src/pages/outgoing_webhooks_2/OutgoingWebhooks2.tsx deleted file mode 100644 index 98cf2c95..00000000 --- a/grafana-plugin/src/pages/outgoing_webhooks_2/OutgoingWebhooks2.tsx +++ /dev/null @@ -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 { - 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) ? ( - - {() => ( - <> - {confirmationModal && ( - - this.setState({ - confirmationModal: undefined, - }) - } - /> - )} - -
- {this.renderOutgoingWebhooksFilters()} - ( -
-
- - Outgoing Webhooks - -
-
- - - - - -
-
- )} - rowKey="id" - columns={columns} - data={webhooks} - /> -
- - {outgoingWebhook2Id && outgoingWebhook2Action && ( - { - this.onDeleteClick(outgoingWebhook2Id).then(() => { - this.setState({ outgoingWebhook2Id: undefined, outgoingWebhook2Action: undefined }); - history.push(`${PLUGIN_ROOT}/outgoing_webhooks`); - }); - }} - /> - )} - - )} -
- ) : ( - Outgoing webhooks 2 functionality is not enabled. - ); - } - - renderOutgoingWebhooksFilters() { - const { query, store } = this.props; - return ( -
- -
- ); - } - - 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 ; - } - - renderActionButtons = (record: OutgoingWebhook2) => { - return ( - ( -
-
this.onLastRunClick(record.id)}> - - View Last Run - -
- -
this.onEditClick(record.id)}> - - Edit settings - -
- -
- 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, - }) - } - > - - {record.is_webhook_enabled ? 'Disable' : 'Enable'} - -
- -
this.onCopyClick(record.id)}> - - Make a copy - -
- - openNotification('Webhook ID has been copied')}> -
- - - UID: {record.id} - -
-
- -
- -
- 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 as ConfirmModalProps, - }) - } - > - - - - Delete Webhook - - -
-
- )} - > - {({ openMenu }) => } - - ); - }; - - renderName(name: String) { - return ( -
- {name} -
- ); - } - - renderUrl(url: string) { - return ( -
- {url} -
- ); - } - - renderLastRun(record: OutgoingWebhook2) { - const lastRunMoment = moment(record.last_response_log?.timestamp); - - return !record.is_webhook_enabled ? ( - Disabled - ) : ( - - {lastRunMoment.isValid() ? lastRunMoment.format('MMM DD, YYYY') : '-'} - {lastRunMoment.isValid() ? lastRunMoment.format('HH:mm') : ''} - - {lastRunMoment.isValid() - ? record.last_response_log?.status_code - ? 'Status: ' + record.last_response_log?.status_code - : 'Check Status' - : ''} - - - ); - } - - onDeleteClick = (id: OutgoingWebhook2['id']): Promise => { - 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)); diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx index a96f090c..83c206cc 100644 --- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx @@ -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) => { - {rootStore.hasFeature(AppFeature.Webhooks2) ? ( - - ) : ( - - )} + diff --git a/grafana-plugin/src/state/features.ts b/grafana-plugin/src/state/features.ts index 624e2761..7636481d 100644 --- a/grafana-plugin/src/state/features.ts +++ b/grafana-plugin/src/state/features.ts @@ -5,5 +5,4 @@ export enum AppFeature { CloudNotifications = 'grafana_cloud_notifications', CloudConnection = 'grafana_cloud_connection', WebSchedules = 'web_schedules', - Webhooks2 = 'webhooks2', } diff --git a/grafana-plugin/src/state/rootBaseStore/index.ts b/grafana-plugin/src/state/rootBaseStore/index.ts index ee81b2d7..ce2f0154 100644 --- a/grafana-plugin/src/state/rootBaseStore/index.ts +++ b/grafana-plugin/src/state/rootBaseStore/index.ts @@ -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() {