Rares/webhook UI changes (#2419)

# What this PR does

## Which issue(s) this PR fixes

## 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:
Rares Mardare 2023-07-05 12:06:33 +03:00 committed by GitHub
parent 5cc06b7041
commit 7195646413
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 506 additions and 261 deletions

View file

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- UI drawer updates for webhooks2 ([#2419](https://github.com/grafana/oncall/pull/2419))
- Removed url from sms notification, changed format ([2317](https://github.com/grafana/oncall/pull/2317))
## v1.3.4 (2023-07-05)

View file

@ -1,9 +1,6 @@
.hamburgerMenu {
cursor: pointer;
color: var(--primary-text-color);
border: var(--border-weak);
border-radius: var(--border-radius);
background-color: var(--button-background);
display: inline-flex;
flex-direction: column;
align-items: center;
@ -11,21 +8,15 @@
justify-content: center;
padding: 4px;
&:hover {
background-color: var(--button-hover-background);
}
&--withBackground {
height: 32px;
width: 30px;
cursor: pointer;
color: var(--primary-text-color);
}
&--small {
height: 24px;
width: 22px;
cursor: pointer;
color: var(--primary-text-color);
}
}

View file

@ -96,7 +96,7 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
)}
</div>
<div className="thin-line-break" />
<div className={cx('thin-line-break')} />
<div className={cx('container', 'container--withTopPadding', 'container--withLateralPadding')}>
<HorizontalGroup justify="space-between">

View file

@ -42,9 +42,6 @@ import { UserActions } from 'utils/authorization';
const cx = cn.bind(styles);
const ACTIONS_LIST_WIDTH = 200;
const ACTIONS_LIST_BORDER = 2;
interface ExpandedIntegrationRouteDisplayProps {
alertReceiveChannelId: AlertReceiveChannel['id'];
channelFilterId: ChannelFilter['id'];
@ -370,7 +367,7 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
</div>
</CopyToClipboard>
<div className="thin-line-break" />
<div className={cx('thin-line-break')} />
<WithPermissionControlTooltip key="delete" userAction={UserActions.IntegrationsWrite}>
<div className={cx('integrations-actionItem')} onClick={onDelete}>
@ -388,8 +385,8 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
{({ openMenu }) => (
<HamburgerMenu
openMenu={openMenu}
listBorder={ACTIONS_LIST_BORDER}
listWidth={ACTIONS_LIST_WIDTH}
listBorder={2}
listWidth={200}
className={'hamburgerMenu--small'}
stopPropagation={true}
/>

View file

@ -9,3 +9,7 @@
.content {
margin: 4px;
}
.tabs__content {
padding-top: 16px;
}

View file

@ -1,15 +1,20 @@
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 Text from 'components/Text/Text';
import OutgoingWebhook2Status from 'containers/OutgoingWebhook2Status/OutgoingWebhook2Status';
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';
@ -19,65 +24,221 @@ const cx = cn.bind(styles);
interface OutgoingWebhook2FormProps {
id: OutgoingWebhook2['id'] | 'new';
action: 'new' | 'update';
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 { id, action, onUpdate, onHide } = props;
const history = useHistory();
const { id, action, onUpdate, onHide, onDelete } = props;
const [activeTab, setActiveTab] = useState<string>(
action === WebhookFormActionType.EDIT_SETTINGS ? WebhookTabs.Settings.key : WebhookTabs.LastRun.key
);
const store = useStore();
const { outgoingWebhook2Store } = store;
const data =
id === 'new'
? { is_webhook_enabled: true, is_legacy: false }
: action === 'new'
? { ...outgoingWebhook2Store.items[id], is_legacy: false, name: '' }
: outgoingWebhook2Store.items[id];
const { outgoingWebhook2Store } = useStore();
const isNew = action === WebhookFormActionType.NEW;
const isNewOrCopy = isNew || action === WebhookFormActionType.COPY;
const handleSubmit = useCallback(
(data: Partial<OutgoingWebhook2>) => {
(action === 'new' ? outgoingWebhook2Store.create(data) : outgoingWebhook2Store.update(id, data)).then(() => {
(isNewOrCopy ? outgoingWebhook2Store.create(data) : outgoingWebhook2Store.update(id, data)).then(() => {
onHide();
onUpdate();
});
},
[id]
);
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;
}
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}>
{renderWebhookForm()}
</Drawer>
);
}
return (
<Drawer
scrollableContent
title={action === 'new' ? 'Create Outgoing Webhook' : 'Edit Outgoing Webhook'}
onClose={onHide}
closeOnMaskClick={false}
>
<div className={cx('content')} data-testid="test__outgoingWebhook2EditForm">
<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" disabled={data.is_legacy}>
{action === 'new' ? 'Create' : 'Update'} Webhook
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
</div>
{data.is_legacy ? (
<div className={cx('content')}>
<Text type="secondary">Legacy migrated webhooks are not editable.</Text>
</div>
) : (
''
)}
// show tabbed drawer (edit/live_run)
<Drawer scrollableContent title={'Outgoing webhook details'} onClose={onHide} closeOnMaskClick={false}>
<TabsBar>
<Tab
key={WebhookTabs.Settings.key}
onChangeTab={() => {
setActiveTab(WebhookTabs.Settings.key);
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/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_2/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}
/>
</Drawer>
);
function renderWebhookForm() {
return (
<>
<div className={cx('content')} data-testid="test__outgoingWebhook2EditForm">
<GForm form={form} data={data} onSubmit={handleSubmit} />
<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;
}
const WebhookTabsContent: React.FC<WebhookTabsProps> = ({
id,
action,
activeTab,
data,
handleSubmit,
onHide,
onUpdate,
onDelete,
}) => {
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">
<GForm form={form} data={data} onSubmit={handleSubmit} />
<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.</Text>
</div>
) : (
''
)}
</>
)}
{activeTab === WebhookTabs.LastRun.key && <OutgoingWebhook2Status id={id} onUpdate={onUpdate} />}
</div>
);
};
export default OutgoingWebhook2Form;

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Drawer, Label, VerticalGroup } from '@grafana/ui';
import { Label, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
@ -16,7 +16,6 @@ const cx = cn.bind(styles);
interface OutgoingWebhook2StatusProps {
id: OutgoingWebhook2['id'];
onHide: () => void;
onUpdate: () => void;
}
@ -49,7 +48,7 @@ function format_response_field(str) {
}
const OutgoingWebhook2Status = observer((props: OutgoingWebhook2StatusProps) => {
const { id, onHide } = props;
const { id } = props;
const store = useStore();
@ -58,76 +57,65 @@ const OutgoingWebhook2Status = observer((props: OutgoingWebhook2StatusProps) =>
const data = outgoingWebhook2Store.items[id];
return (
<Drawer
scrollableContent
title={
<Text.Title className={cx('title')} level={4}>
Outgoing Webhook Status
</Text.Title>
}
onClose={onHide}
closeOnMaskClick
>
<div className={cx('content')}>
<VerticalGroup>
<Label>Webhook Name</Label>
<SourceCode showClipboardIconOnly>{data.name}</SourceCode>
<Label>Webhook ID</Label>
<SourceCode showClipboardIconOnly>{data.id}</SourceCode>
<Label>Trigger Type</Label>
<SourceCode showClipboardIconOnly>{data.trigger_type_name}</SourceCode>
<div className={cx('content')}>
<VerticalGroup>
<Label>Webhook Name</Label>
<SourceCode showClipboardIconOnly>{data.name}</SourceCode>
<Label>Webhook ID</Label>
<SourceCode showClipboardIconOnly>{data.id}</SourceCode>
<Label>Trigger Type</Label>
<SourceCode showClipboardIconOnly>{data.trigger_type_name}</SourceCode>
{data.last_response_log.timestamp ? (
<VerticalGroup>
<Label>Last Run Time</Label>
<SourceCode showClipboardIconOnly>{data.last_response_log.timestamp}</SourceCode>
{data.last_response_log.timestamp ? (
<VerticalGroup>
<Label>Last Run Time</Label>
<SourceCode showClipboardIconOnly>{data.last_response_log.timestamp}</SourceCode>
{data.last_response_log.url && (
<Debug title="URL" source={data.url} result={data.last_response_log.url}></Debug>
)}
{data.last_response_log.status_code && (
<VerticalGroup>
<Label>Response Code</Label>
<SourceCode showClipboardIconOnly>{data.last_response_log.status_code}</SourceCode>
</VerticalGroup>
)}
{data.last_response_log.url && (
<Debug title="URL" source={data.url} result={data.last_response_log.url}></Debug>
)}
{data.last_response_log.status_code && (
<VerticalGroup>
<Label>Response Code</Label>
<SourceCode showClipboardIconOnly>{data.last_response_log.status_code}</SourceCode>
</VerticalGroup>
)}
{data.last_response_log.content && (
<VerticalGroup>
<Label>Response Body</Label>
<SourceCode showClipboardIconOnly>{format_response_field(data.last_response_log.content)}</SourceCode>
</VerticalGroup>
)}
{data.last_response_log.request_trigger && (
<Debug
title="Trigger Template"
source={data.trigger_template}
result={data.last_response_log.request_trigger}
></Debug>
)}
{data.last_response_log.request_headers && (
<Debug
title="Request Headers"
source={data.headers}
result={data.last_response_log.request_headers}
></Debug>
)}
{data.last_response_log.request_data && (
<Debug
title="Request Data"
source={data.data}
result={format_response_field(data.last_response_log.request_data)}
></Debug>
)}
</VerticalGroup>
) : (
<Text type="primary" size="medium">
An event triggering this webhook has not been sent yet!
</Text>
)}
</VerticalGroup>
</div>
</Drawer>
{data.last_response_log.content && (
<VerticalGroup>
<Label>Response Body</Label>
<SourceCode showClipboardIconOnly>{format_response_field(data.last_response_log.content)}</SourceCode>
</VerticalGroup>
)}
{data.last_response_log.request_trigger && (
<Debug
title="Trigger Template"
source={data.trigger_template}
result={data.last_response_log.request_trigger}
></Debug>
)}
{data.last_response_log.request_headers && (
<Debug
title="Request Headers"
source={data.headers}
result={data.last_response_log.request_headers}
></Debug>
)}
{data.last_response_log.request_data && (
<Debug
title="Request Data"
source={data.data}
result={format_response_field(data.last_response_log.request_data)}
></Debug>
)}
</VerticalGroup>
) : (
<Text type="primary" size="medium">
An event triggering this webhook has not been sent yet!
</Text>
)}
</VerticalGroup>
</div>
);
});

View file

@ -88,8 +88,6 @@ interface IntegrationState extends PageBaseState {
openRoutes: string[];
}
const ACTIONS_LIST_WIDTH = 200;
const ACTIONS_LIST_BORDER = 2;
const NEW_ROUTE_DEFAULT = '';
@observer
@ -685,7 +683,7 @@ const IntegrationSendDemoPayloadModal: React.FC<IntegrationSendDemoPayloadModalP
<Button variant={'secondary'} onClick={onHideOrCancel}>
Cancel
</Button>
<CopyToClipboard text={getCurlText()} onCopy={() => openNotification('CURL copied!')}>
<CopyToClipboard text={getCurlText()} onCopy={() => openNotification('CURL has been copied')}>
<Button variant={'secondary'}>Copy as CURL</Button>
</CopyToClipboard>
<Button variant={'primary'} onClick={sendDemoAlert} data-testid="submit-send-alert">
@ -884,7 +882,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
</div>
</CopyToClipboard>
<div className="thin-line-break" />
<div className={cx('thin-line-break')} />
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<div className={cx('integration__actionItem')}>
@ -918,14 +916,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
</div>
)}
>
{({ openMenu }) => (
<HamburgerMenu
openMenu={openMenu}
listBorder={ACTIONS_LIST_BORDER}
listWidth={ACTIONS_LIST_WIDTH}
withBackground
/>
)}
{({ openMenu }) => <HamburgerMenu openMenu={openMenu} listBorder={2} listWidth={200} withBackground />}
</WithContextMenu>
</div>
</>

View file

@ -40,8 +40,6 @@ const cx = cn.bind(styles);
const FILTERS_DEBOUNCE_MS = 500;
const ITEMS_PER_PAGE = 15;
const MAX_LINE_LENGTH = 40;
const ACTIONS_LIST_WIDTH = 200;
const ACTIONS_LIST_BORDER = 2;
interface IntegrationsState extends PageBaseState {
integrationsFilters: Filters;
@ -402,7 +400,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
</div>
</WithPermissionControlTooltip>
<CopyToClipboard text={item.id} onCopy={() => openNotification('Integration ID is copied')}>
<CopyToClipboard text={item.id} onCopy={() => openNotification('Integration ID has been copied')}>
<div className={cx('integrations-actionItem')}>
<HorizontalGroup spacing={'xs'}>
<Icon name="copy" />
@ -412,7 +410,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
</div>
</CopyToClipboard>
<div className="thin-line-break" />
<div className={cx('thin-line-break')} />
<WithPermissionControlTooltip key="delete" userAction={UserActions.IntegrationsWrite}>
<div className={cx('integrations-actionItem')}>
@ -447,9 +445,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
</div>
)}
>
{({ openMenu }) => (
<HamburgerMenu openMenu={openMenu} listBorder={ACTIONS_LIST_BORDER} listWidth={ACTIONS_LIST_WIDTH} />
)}
{({ openMenu }) => <HamburgerMenu openMenu={openMenu} listBorder={2} listWidth={200} />}
</WithContextMenu>
);
};

View file

@ -1,15 +0,0 @@
.header {
display: flex;
align-items: center;
width: 100%;
padding-top: 12px;
}
.header__title {
display: flex;
align-items: baseline;
}
.header__desc {
margin-bottom: 12px;
}

View file

@ -0,0 +1,41 @@
.header {
display: flex;
align-items: center;
width: 100%;
padding-top: 12px;
}
.header__title {
display: flex;
align-items: baseline;
}
.header__desc {
margin-bottom: 12px;
}
.hamburgerMenu {
display: flex;
flex-direction: column;
width: 225px;
border-radius: 2px;
}
.hamburgerMenu__item {
padding: 8px;
display: flex;
align-items: center;
flex-direction: row;
flex-shrink: 0;
white-space: nowrap;
border-left: 2px solid transparent;
cursor: pointer;
min-width: 84px;
display: flex;
gap: 8px;
flex-direction: row;
&:hover {
background: var(--cards-background);
}
}

View file

@ -1,6 +1,15 @@
import React from 'react';
import { Button, HorizontalGroup, Icon, IconButton, VerticalGroup } 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';
@ -9,6 +18,7 @@ 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,
@ -16,50 +26,45 @@ 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 OutgoingWebhook2Form from 'containers/OutgoingWebhook2Form/OutgoingWebhook2Form';
import OutgoingWebhook2Status from 'containers/OutgoingWebhook2Status/OutgoingWebhook2Status';
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 { 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.css';
import styles from './OutgoingWebhooks2.module.scss';
import { WebhookFormActionType } from './OutgoingWebhooks2.types';
const cx = cn.bind(styles);
const Action = {
STATUS: 'status',
EDIT: 'edit',
COPY: 'copy',
};
interface OutgoingWebhooks2Props
extends WithStoreProps,
PageProps,
RouteComponentProps<{ id: string; action: string }> {}
interface OutgoingWebhooks2State extends PageBaseState {
outgoingWebhook2Action?: 'new' | 'update';
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) {
if (prevProps.match.params.id !== this.props.match.params.id && !this.state.outgoingWebhook2Action) {
this.parseQueryParams();
}
}
@ -77,37 +82,36 @@ class OutgoingWebhooks2 extends React.Component<OutgoingWebhooks2Props, Outgoing
},
} = this.props;
if (!id) {
return;
if (action) {
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: convertWebhookUrlToAction(action) });
}
let outgoingWebhook2: OutgoingWebhook2 | void = undefined;
const isNewWebhook = id === 'new';
if (!isNewWebhook) {
outgoingWebhook2 = await store.outgoingWebhook2Store
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) } }));
}
if (isNewWebhook || (action === Action.COPY && outgoingWebhook2)) {
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: 'new' });
} else if (action === Action.EDIT && outgoingWebhook2) {
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: 'update' });
} else if (action === Action.STATUS && outgoingWebhook2) {
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: undefined });
.catch((error) =>
this.setState({ errorData: { ...getWrongTeamResponseInfo(error) }, outgoingWebhook2Action: undefined })
);
}
};
update = () => {
const { store } = this.props;
return store.outgoingWebhook2Store.updateItems();
};
render() {
const { store, query } = this.props;
const { outgoingWebhook2Id, outgoingWebhook2Action, errorData } = this.state;
const {
store,
history,
match: {
params: { id },
},
} = this.props;
const { outgoingWebhook2Id, outgoingWebhook2Action, errorData, confirmationModal } = this.state;
const webhooks = store.outgoingWebhook2Store.getSearchResult();
@ -151,10 +155,21 @@ class OutgoingWebhooks2 extends React.Component<OutgoingWebhooks2Props, Outgoing
errorData={errorData}
objectName="outgoing webhook 2"
pageName="outgoing_webhooks_2"
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
@ -192,19 +207,19 @@ class OutgoingWebhooks2 extends React.Component<OutgoingWebhooks2Props, Outgoing
data={webhooks}
/>
</div>
{outgoingWebhook2Id && outgoingWebhook2Action && (
<OutgoingWebhook2Form
id={outgoingWebhook2Id}
action={outgoingWebhook2Action}
onUpdate={this.update}
onHide={this.handleOutgoingWebhookFormHide}
/>
)}
{outgoingWebhook2Id && !outgoingWebhook2Action && (
<OutgoingWebhook2Status
id={outgoingWebhook2Id}
onUpdate={this.update}
onHide={this.handleOutgoingWebhookFormHide}
onDelete={() => {
this.onDeleteClick(outgoingWebhook2Id).then(() => {
this.setState({ outgoingWebhook2Id: undefined, outgoingWebhook2Action: undefined });
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2`);
});
}}
/>
)}
</>
@ -245,53 +260,86 @@ class OutgoingWebhooks2 extends React.Component<OutgoingWebhooks2Props, Outgoing
return <TeamName team={teams[record.team]} />;
}
renderActionButtons = (record: ActionDTO) => {
renderActionButtons = (record: OutgoingWebhook2) => {
return (
<HorizontalGroup justify="flex-end">
<CopyToClipboard text={record.id}>
<IconButton
variant="primary"
tooltip={
<div>
ID {record.id}
<br />
(click to copy ID to clipboard)
<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>
}
tooltipPlacement="top"
name="info-circle"
/>
</CopyToClipboard>
<WithPermissionControlTooltip key={'status_action'} userAction={UserActions.OutgoingWebhooksRead}>
<IconButton
tooltip="Status"
tooltipPlacement="top"
name="history"
onClick={() => this.onStatusClick(record.id)}
/>
</WithPermissionControlTooltip>
<WithPermissionControlTooltip key={'edit_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<IconButton tooltip="Edit" tooltipPlacement="top" name="cog" onClick={() => this.onEditClick(record.id)} />
</WithPermissionControlTooltip>
<WithPermissionControlTooltip key={'copy_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<IconButton
tooltip="Make a copy"
tooltipPlacement="top"
name="copy"
onClick={() => this.onCopyClick(record.id)}
/>
</WithPermissionControlTooltip>
<WithPermissionControlTooltip key={'delete_action'} userAction={UserActions.OutgoingWebhooksWrite}>
<WithConfirm title={`Are you sure to remove "${record.name}"?`} confirmText="Remove">
<IconButton
tooltip="Remove"
tooltipPlacement="top"
onClick={this.getDeleteClickHandler(record.id)}
name="trash-alt"
/>
</WithConfirm>
</WithPermissionControlTooltip>
</HorizontalGroup>
</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>
);
};
@ -331,36 +379,56 @@ class OutgoingWebhooks2 extends React.Component<OutgoingWebhooks2Props, Outgoing
);
}
getDeleteClickHandler = (id: OutgoingWebhook2['id']) => {
onDeleteClick = (id: OutgoingWebhook2['id']): Promise<void> => {
const { store } = this.props;
return () => {
store.outgoingWebhook2Store.delete(id).then(this.update);
};
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: 'update' });
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/edit/${id}`);
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: WebhookFormActionType.EDIT_SETTINGS }, () =>
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/edit/${id}`)
);
};
onCopyClick = (id: OutgoingWebhook2['id']) => {
const { history } = this.props;
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: 'new' });
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/copy/${id}`);
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: WebhookFormActionType.COPY }, () =>
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/copy/${id}`)
);
};
onStatusClick = (id: OutgoingWebhook2['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: undefined });
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/status/${id}`);
this.setState({ outgoingWebhook2Id: id, outgoingWebhook2Action: WebhookFormActionType.VIEW_LAST_RUN }, () =>
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/last_run/${id}`)
);
};
handleOutgoingWebhookFormHide = () => {
@ -372,6 +440,18 @@ class OutgoingWebhooks2 extends React.Component<OutgoingWebhooks2Props, Outgoing
};
}
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

@ -0,0 +1,6 @@
export enum WebhookFormActionType {
NEW = 1,
COPY = 2,
VIEW_LAST_RUN = 3,
EDIT_SETTINGS = 4,
}

View file

@ -80,3 +80,7 @@
.u-margin-right-md {
margin-right: 8px;
}
.buttons {
padding-bottom: 24px;
}