Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rares/webhook UI changes #2419

Merged
merged 23 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
.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;
vertical-align: middle;
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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}>
Expand All @@ -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}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@
.content {
margin: 4px;
}

.tabs__content {
padding-top: 16px;
}
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what the pros of using this data structure?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a key-value structure for when you need to keep groups of key/value pairs. It's used in other places as well.

import { UserActions } from 'utils/authorization';
import { PLUGIN_ROOT } from 'utils/consts';

import { form } from './OutgoingWebhook2Form.config';

Expand All @@ -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;
}

const OutgoingWebhook2Form = observer((props: OutgoingWebhook2FormProps) => {
const { id, action, onUpdate, onHide } = props;

const store = useStore();
export const WebhookTabs = {
Settings: new KeyValuePair('Settings', 'Settings'),
LastRun: new KeyValuePair('LastRun', 'Last Run'),
};

const { outgoingWebhook2Store } = store;
const OutgoingWebhook2Form = observer((props: OutgoingWebhook2FormProps) => {
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 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can it be unloaded in store yet at the moment this code is running?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean by this. This is when you're hitting an ID that was previously deleted or it just doesn't exist. There's no such object in the store.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as I understand we don't show anything if there is no such item in the store, but in this case we need to force load it within this component

return null;
}

if (action === WebhookFormActionType.NEW || action === WebhookFormActionType.COPY) {
// show just the creation form, not the tabs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mabe just make LastRun tab disabled for those cases?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the figma where on new/copy there's no tabs shown, plus the content is slightly different.

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;
Loading