Added entity selection changes to webhook (#2777)

* Implemented Listing, Add and Delete Webhook functionalities

* Implementing Edit Webhook and minor changes

* Minor updates

* Minor changes for generate button

* Addressing comments

* Addressing comment

* Added entity selection changes to webhook

* Addressing change requests

* Minor changes

* Removed unnecessary file

* Update openmetadata-ui/src/main/resources/ui/src/components/common/webhook-data-card/WebhookDataCard.tsx

Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>

* Addressing change requests

* Schema to Typescript interfaces for webhook

Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
darth-coder00 2022-02-18 11:12:04 +05:30 committed by GitHub
parent 891ff465a0
commit 78ee32f585
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 509 additions and 278 deletions

View File

@ -12,13 +12,18 @@
*/
import { LoadingState } from 'Models';
import { FormSubmitType } from '../../enums/form.enum';
import { CreateWebhook } from '../../generated/api/events/createWebhook';
import { Webhook } from '../../generated/entity/events/webhook';
export interface AddWebhookProps {
data?: Webhook;
header: string;
mode: FormSubmitType;
saveState?: LoadingState;
deleteState?: LoadingState;
allowAccess?: boolean;
onCancel: () => void;
onDelete?: (id: string) => void;
onSave: (data: CreateWebhook) => void;
}

View File

@ -17,7 +17,9 @@ import { cloneDeep, isEmpty, isNil, startCase } from 'lodash';
import { EditorContentRef } from 'Models';
import React, { FunctionComponent, useRef, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { WILD_CARD_CHAR } from '../../constants/char.constants';
import { EntityType } from '../../enums/entity.enum';
import { FormSubmitType } from '../../enums/form.enum';
import { PageLayoutType } from '../../enums/layout.enum';
import {
CreateWebhook,
@ -38,24 +40,87 @@ import MarkdownWithPreview from '../common/editor/MarkdownWithPreview';
import PageLayout from '../containers/PageLayout';
import DropDown from '../dropdown/DropDown';
import Loader from '../Loader/Loader';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import { AddWebhookProps } from './AddWebhook.interface';
const Field = ({ children }: { children: React.ReactNode }) => {
return <div className="tw-mt-4">{children}</div>;
};
const getEntitiesList = () => {
const retVal: Array<{ name: string; value: string }> = [
EntityType.TABLE,
EntityType.TOPIC,
EntityType.DASHBOARD,
EntityType.PIPELINE,
].map((item) => {
return {
name: startCase(item),
value: item,
};
});
retVal.unshift({ name: 'All entities', value: WILD_CARD_CHAR });
return retVal;
};
const getHiddenEntitiesList = (entities: Array<string> = []) => {
if (entities.includes(WILD_CARD_CHAR)) {
return entities.filter((item) => item !== WILD_CARD_CHAR);
} else {
return undefined;
}
};
const getSelectedEvents = (prev: EventFilter, value: string) => {
let entities = prev.entities || [];
if (entities.includes(value)) {
if (value === WILD_CARD_CHAR) {
entities = [];
} else {
if (entities.includes(WILD_CARD_CHAR)) {
const allIndex = entities.indexOf(WILD_CARD_CHAR);
entities.splice(allIndex, 1);
}
const index = entities.indexOf(value);
entities.splice(index, 1);
}
} else {
if (value === WILD_CARD_CHAR) {
entities = getEntitiesList().map((item) => item.value);
} else {
entities.push(value);
}
}
return { ...prev, entities };
};
const getEventFilterByType = (
filters: Array<EventFilter>,
type: EventType
): EventFilter => {
return filters.find((item) => item.eventType === type) || ({} as EventFilter);
let eventFilter =
filters.find((item) => item.eventType === type) || ({} as EventFilter);
if (eventFilter.entities?.includes(WILD_CARD_CHAR)) {
eventFilter = getSelectedEvents(
{ ...eventFilter, entities: [] },
WILD_CARD_CHAR
);
}
return eventFilter;
};
const AddWebhook: FunctionComponent<AddWebhookProps> = ({
data,
header,
mode = FormSubmitType.ADD,
saveState = 'initial',
deleteState = 'initial',
allowAccess = true,
onCancel,
onDelete,
onSave,
}: AddWebhookProps) => {
const markdownRef = useRef<EditorContentRef>();
@ -93,13 +158,25 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
endpointUrl: false,
eventFilters: false,
invalidEndpointUrl: false,
invalidEventFilters: false,
});
const [copiedSecret, setCopiedSecret] = useState<boolean>(false);
const [generatingSecret, setGeneratingSecret] = useState<boolean>(false);
const [isDelete, setIsDelete] = useState<boolean>(false);
const handleDelete = () => {
if (data) {
onDelete && onDelete(data.id);
}
setIsDelete(false);
};
const handleValidation = (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
if (!allowAccess) {
return;
}
const value = event.target.value;
const eleName = event.target.name;
let { name, endpointUrl, invalidEndpointUrl } = cloneDeep(showErrorMsg);
@ -135,6 +212,9 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
};
const generateSecret = () => {
if (!allowAccess) {
return;
}
const apiKey = cryptoRandomString({ length: 50, type: 'alphanumeric' });
setGeneratingSecret(true);
setTimeout(() => {
@ -149,44 +229,10 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
setSecretKey('');
};
const getEntitiesList = () => {
const retVal: Array<{ name: string; value: string }> = [
EntityType.TABLE,
EntityType.TOPIC,
EntityType.DASHBOARD,
EntityType.PIPELINE,
].map((item) => {
return {
name: startCase(item),
value: item,
};
});
retVal.unshift({ name: 'All entities', value: '*' });
return retVal;
};
const getSelectedEvents = (prev: EventFilter, value: string) => {
let entities = prev.entities || [];
if (entities.includes(value)) {
const index = entities.indexOf(value);
entities.splice(index, 1);
} else {
if (value === '*') {
entities = [value];
} else {
if (value !== '*' && entities.includes('*')) {
const allIndex = entities.indexOf('*');
entities.splice(allIndex, 1);
}
entities.push(value);
}
}
return { ...prev, entities };
};
const toggleEventFilters = (type: EventType, value: boolean) => {
if (!allowAccess) {
return;
}
let setter;
switch (type) {
case EventType.EntityCreated: {
@ -214,7 +260,34 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
: ({} as EventFilter)
);
setShowErrorMsg((prev) => {
return { ...prev, eventFilters: false };
return { ...prev, eventFilters: false, invalidEventFilters: false };
});
}
};
const handleEntitySelection = (type: EventType, value: string) => {
let setter;
switch (type) {
case EventType.EntityCreated: {
setter = setCreateEvents;
break;
}
case EventType.EntityUpdated: {
setter = setUpdateEvents;
break;
}
case EventType.EntityDeleted: {
setter = setDeleteEvents;
break;
}
}
if (setter) {
setter((prev) => getSelectedEvents(prev, value));
setShowErrorMsg((prev) => {
return { ...prev, eventFilters: false, invalidEventFilters: false };
});
}
};
@ -222,18 +295,42 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
const getEventFiltersData = () => {
const eventFilters: Array<EventFilter> = [];
if (!isEmpty(createEvents)) {
eventFilters.push(createEvents);
const event = createEvents.entities?.includes(WILD_CARD_CHAR)
? { ...createEvents, entities: [WILD_CARD_CHAR] }
: createEvents;
eventFilters.push(event);
}
if (!isEmpty(updateEvents)) {
eventFilters.push(updateEvents);
const event = updateEvents.entities?.includes(WILD_CARD_CHAR)
? { ...updateEvents, entities: [WILD_CARD_CHAR] }
: updateEvents;
eventFilters.push(event);
}
if (!isEmpty(deleteEvents)) {
eventFilters.push(deleteEvents);
const event = deleteEvents.entities?.includes(WILD_CARD_CHAR)
? { ...deleteEvents, entities: [WILD_CARD_CHAR] }
: deleteEvents;
eventFilters.push(event);
}
return eventFilters;
};
const validateEventFilters = () => {
let isValid = false;
if (!isEmpty(createEvents)) {
isValid = Boolean(createEvents.entities?.length);
}
if (!isEmpty(updateEvents)) {
isValid = Boolean(updateEvents.entities?.length);
}
if (!isEmpty(deleteEvents) && deleteEvents.entities?.length) {
isValid = Boolean(deleteEvents.entities?.length);
}
return isValid;
};
const validateForm = () => {
const errMsg = {
name: !name.trim(),
@ -244,6 +341,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
...deleteEvents,
}),
invalidEndpointUrl: !isValidUrl(endpointUrl.trim()),
invalidEventFilters: !validateEventFilters(),
};
setShowErrorMsg(errMsg);
@ -266,6 +364,73 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
}
};
const getDeleteButton = () => {
return allowAccess ? (
<>
{deleteState === 'waiting' ? (
<Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular"
theme="primary"
variant="text">
<Loader size="small" type="default" />
</Button>
) : (
<Button
className={classNames({
'tw-opacity-40': !allowAccess,
})}
data-testid="delete-webhook"
size="regular"
theme="primary"
variant="text"
onClick={() => setIsDelete(true)}>
Delete
</Button>
)}
</>
) : null;
};
const getSaveButton = () => {
return allowAccess ? (
<>
{saveState === 'waiting' ? (
<Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular"
theme="primary"
variant="contained">
<Loader size="small" type="white" />
</Button>
) : saveState === 'success' ? (
<Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular"
theme="primary"
variant="contained">
<i aria-hidden="true" className="fa fa-check" />
</Button>
) : (
<Button
className={classNames('tw-w-16 tw-h-10', {
'tw-opacity-40': !allowAccess,
})}
data-testid="save-webhook"
size="regular"
theme="primary"
variant="contained"
onClick={handleSave}>
Save
</Button>
)}
</>
) : null;
};
const fetchRightPanel = () => {
return (
<>
@ -333,6 +498,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
</label>
<MarkdownWithPreview
data-testid="description"
readonly={!allowAccess}
ref={markdownRef}
value={description}
/>
@ -344,6 +510,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
<input
className="tw-form-inputs tw-px-3 tw-py-1"
data-testid="endpoint-url"
disabled={!allowAccess}
id="endpoint-url"
name="endpoint-url"
placeholder="http(s)://www.example.com"
@ -364,7 +531,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
className={classNames('toggle-switch', { open: active })}
data-testid="active"
onClick={() => {
setActive((prev) => !prev);
allowAccess && setActive((prev) => !prev);
}}>
<div className="switch" />
</div>
@ -385,6 +552,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
checked={!isEmpty(createEvents)}
className="tw-mr-1 custom-checkbox"
data-testid="checkbox"
disabled={!allowAccess}
type="checkbox"
onChange={(e) => {
toggleEventFilters(EventType.EntityCreated, e.target.checked);
@ -399,15 +567,14 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
</div>
<DropDown
className="tw-bg-white"
disabled={isEmpty(createEvents)}
disabled={!allowAccess || isEmpty(createEvents)}
dropDownList={getEntitiesList()}
hiddenItems={getHiddenEntitiesList(createEvents.entities)}
label="select entities"
selectedItems={createEvents.entities}
type="checkbox"
onSelect={(_e, value) =>
setCreateEvents((prev) =>
getSelectedEvents(prev, value as string)
)
handleEntitySelection(EventType.EntityCreated, value as string)
}
/>
</Field>
@ -420,6 +587,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
checked={!isEmpty(updateEvents)}
className="tw-mr-1 custom-checkbox"
data-testid="checkbox"
disabled={!allowAccess}
type="checkbox"
onChange={(e) => {
toggleEventFilters(EventType.EntityUpdated, e.target.checked);
@ -434,15 +602,14 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
</div>
<DropDown
className="tw-bg-white"
disabled={isEmpty(updateEvents)}
disabled={!allowAccess || isEmpty(updateEvents)}
dropDownList={getEntitiesList()}
hiddenItems={getHiddenEntitiesList(updateEvents.entities)}
label="select entities"
selectedItems={updateEvents.entities}
type="checkbox"
onSelect={(_e, value) =>
setUpdateEvents((prev) =>
getSelectedEvents(prev, value as string)
)
handleEntitySelection(EventType.EntityUpdated, value as string)
}
/>
</Field>
@ -455,6 +622,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
checked={!isEmpty(deleteEvents)}
className="tw-mr-1 custom-checkbox"
data-testid="checkbox"
disabled={!allowAccess}
type="checkbox"
onChange={(e) => {
toggleEventFilters(EventType.EntityDeleted, e.target.checked);
@ -469,19 +637,21 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
</div>
<DropDown
className="tw-bg-white"
disabled={isEmpty(deleteEvents)}
disabled={!allowAccess || isEmpty(deleteEvents)}
dropDownList={getEntitiesList()}
hiddenItems={getHiddenEntitiesList(deleteEvents.entities)}
label="select entities"
selectedItems={deleteEvents.entities}
type="checkbox"
onSelect={(_e, value) =>
setDeleteEvents((prev) =>
getSelectedEvents(prev, value as string)
)
handleEntitySelection(EventType.EntityDeleted, value as string)
}
/>
{showErrorMsg.eventFilters &&
errorMsg('Webhook event filters are required.')}
{showErrorMsg.eventFilters
? errorMsg('Webhook event filters are required.')
: showErrorMsg.invalidEventFilters
? errorMsg('Webhook event filters are invalid.')
: null}
</Field>
<Field>
<div className="tw-flex tw-justify-end tw-pt-1">
@ -512,6 +682,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
<input
className="tw-form-inputs tw-px-3 tw-py-1"
data-testid="batch-size"
disabled={!allowAccess}
id="batch-size"
name="batch-size"
placeholder="10"
@ -529,6 +700,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
<input
className="tw-form-inputs tw-px-3 tw-py-1"
data-testid="connection-timeout"
disabled={!allowAccess}
id="connection-timeout"
name="connection-timeout"
placeholder="10"
@ -540,143 +712,146 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
</div>
</Field>
<Field>
{!data ? (
<>
<label
className="tw-block tw-form-label tw-my-0"
htmlFor="secret-key">
Secret Key:
</label>
{allowAccess ? (
!data ? (
<>
<label
className="tw-block tw-form-label tw-my-0"
htmlFor="secret-key">
Secret Key:
</label>
<div className="tw-flex tw-items-center">
<input
readOnly
className="tw-form-inputs tw-px-3 tw-py-1"
data-testid="connection-timeout"
id="connection-timeout"
name="connection-timeout"
placeholder="secret key"
type="text"
value={secretKey}
/>
<Button
className="tw-w-8 tw-h-8 tw--ml-8 tw-rounded-md"
data-testid="generate-secret"
size="custom"
theme="default"
variant="text"
onClick={generateSecret}>
{generatingSecret ? (
<Loader size="small" type="default" />
) : (
<i className="fas fa-sync-alt" />
)}
</Button>
{secretKey ? (
<>
<CopyToClipboard
text={secretKey}
onCopy={() => setCopiedSecret(true)}>
<Button
className="tw-h-8 tw-ml-4"
data-testid="copy-secret"
size="custom"
theme="default"
variant="text">
<SVGIcons
alt="Copy"
icon={Icons.COPY}
width="16px"
/>
</Button>
</CopyToClipboard>
<Button
className="tw-h-8 tw-ml-4"
data-testid="clear-secret"
size="custom"
theme="default"
variant="text"
onClick={resetSecret}>
<SVGIcons
alt="Delete"
icon={Icons.DELETE}
width="16px"
/>
</Button>
</>
) : null}
</div>
</>
) : data.secretKey ? (
<div className="tw-flex tw-items-center">
<input
readOnly
className="tw-form-inputs tw-px-3 tw-py-1"
data-testid="connection-timeout"
id="connection-timeout"
name="connection-timeout"
data-testid="secret-key"
id="secret-key"
name="secret-key"
placeholder="secret key"
type="text"
value={secretKey}
/>
<Button
className="tw-w-8 tw-h-8 tw--ml-8 tw-rounded-md"
data-testid="generate-secret"
size="custom"
theme="default"
variant="text"
onClick={generateSecret}>
{generatingSecret ? (
<Loader size="small" type="default" />
) : (
<i className="fas fa-sync-alt" />
)}
</Button>
{secretKey ? (
<>
<CopyToClipboard
text={secretKey}
onCopy={() => setCopiedSecret(true)}>
<Button
className="tw-h-8 tw-ml-4"
data-testid="copy-secret"
size="custom"
theme="default"
variant="text">
<SVGIcons
alt="Copy"
icon={Icons.COPY}
width="16px"
/>
</Button>
</CopyToClipboard>
<Button
className="tw-h-8 tw-ml-4"
data-testid="clear-secret"
size="custom"
theme="default"
variant="text"
onClick={resetSecret}>
<SVGIcons
alt="Delete"
icon={Icons.DELETE}
width="16px"
/>
</Button>
</>
) : null}
<CopyToClipboard
text={secretKey}
onCopy={() => setCopiedSecret(true)}>
<Button
className="tw-h-8 tw-ml-4"
data-testid="copy-secret"
size="custom"
theme="default"
variant="text">
<SVGIcons alt="Copy" icon={Icons.COPY} width="16px" />
</Button>
</CopyToClipboard>
</div>
</>
) : data.secretKey ? (
<div className="tw-flex tw-items-center">
<input
readOnly
className="tw-form-inputs tw-px-3 tw-py-1"
data-testid="secret-key"
id="secret-key"
name="secret-key"
placeholder="secret key"
type="text"
value={secretKey}
/>
<CopyToClipboard
text={secretKey}
onCopy={() => setCopiedSecret(true)}>
<Button
className="tw-h-8 tw-ml-4"
data-testid="copy-secret"
size="custom"
theme="default"
variant="text">
<SVGIcons alt="Copy" icon={Icons.COPY} width="16px" />
</Button>
</CopyToClipboard>
</div>
) : null
) : null}
{copiedSecret && validMsg('Copied to clipboard.')}
{copiedSecret && validMsg('Copied to the clipboard.')}
</Field>
</>
) : null}
<Field>
<div className="tw-flex tw-justify-end">
<Button
data-testid="cancel-webhook"
size="regular"
theme="primary"
variant="text"
onClick={onCancel}>
Discard
</Button>
{saveState === 'waiting' ? (
{data && mode === 'edit' ? (
<div className="tw-flex tw-justify-between">
<Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
data-testid="cancel-webhook"
size="regular"
theme="primary"
variant="contained">
<Loader size="small" type="white" />
variant="outlined"
onClick={onCancel}>
<i className="fas fa-arrow-left tw-text-sm tw-align-middle tw-pr-1.5" />{' '}
<span>Back</span>
</Button>
) : saveState === 'success' ? (
<div className="tw-flex tw-justify-end">
{getDeleteButton()}
{getSaveButton()}
</div>
</div>
) : (
<div className="tw-flex tw-justify-end">
<Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
data-testid="cancel-webhook"
size="regular"
theme="primary"
variant="contained">
<i aria-hidden="true" className="fa fa-check" />
variant="text"
onClick={onCancel}>
Discard
</Button>
) : (
<Button
className="tw-w-16 tw-h-10"
data-testid="save-webhook"
size="regular"
theme="primary"
variant="contained"
onClick={handleSave}>
Save
</Button>
)}
</div>
{getSaveButton()}
</div>
)}
</Field>
{data && isDelete && (
<ConfirmationModal
bodyText={`You want to delete webhook ${data.name} permanently? This action cannot be reverted.`}
cancelText="Discard"
confirmButtonCss="tw-bg-error hover:tw-bg-error focus:tw-bg-error"
confirmText="Delete"
header="Are you sure?"
onCancel={() => setIsDelete(false)}
onConfirm={handleDelete}
/>
)}
</div>
</PageLayout>
);

View File

@ -18,7 +18,6 @@ export interface WebhooksProps {
data: Array<Webhook>;
paging: Paging;
onAddWebhook: () => void;
onDeleteWebhook: (id: string) => void;
onEditWebhook: (name: string) => void;
onClickWebhook: (name: string) => void;
onPageChange: (type: string) => void;
}

View File

@ -13,27 +13,27 @@
import classNames from 'classnames';
import { isNil, startCase } from 'lodash';
import React, { FunctionComponent, useState } from 'react';
import React, { FunctionComponent } from 'react';
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
import { Status, Webhook } from '../../generated/entity/events/webhook';
import { Status } from '../../generated/entity/events/webhook';
import { useAuth } from '../../hooks/authHooks';
import { getDocButton } from '../../utils/CommonUtils';
import { Button } from '../buttons/Button/Button';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import NextPrevious from '../common/next-previous/NextPrevious';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import WebhookDataCard from '../common/webhook-data-card/WebhookDataCard';
import PageLayout from '../containers/PageLayout';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import { WebhooksProps } from './Webhooks.interface';
const statuses = [
{
name: startCase(Status.NotStarted),
value: Status.NotStarted,
name: startCase(Status.Disabled),
value: Status.Disabled,
},
{
name: startCase(Status.Started),
value: Status.Started,
name: startCase(Status.Active),
value: Status.Active,
},
{
name: startCase(Status.Failed),
@ -53,19 +53,10 @@ const Webhooks: FunctionComponent<WebhooksProps> = ({
data = [],
paging,
onAddWebhook,
onDeleteWebhook,
onEditWebhook,
onClickWebhook,
onPageChange,
}: WebhooksProps) => {
const { isAuthDisabled, isAdminUser } = useAuth();
const [deleteData, setDeleteData] = useState<Webhook>();
const handleDelete = () => {
if (deleteData) {
onDeleteWebhook(deleteData.id);
}
setDeleteData(undefined);
};
const fetchLeftPanel = () => {
return (
@ -117,7 +108,7 @@ const Webhooks: FunctionComponent<WebhooksProps> = ({
);
};
return (
return data.length ? (
<PageLayout leftPanel={fetchLeftPanel()} rightPanel={fetchRightPanel()}>
<div className="">
<div className="tw-flex tw-justify-end tw-items-center">
@ -142,31 +133,36 @@ const Webhooks: FunctionComponent<WebhooksProps> = ({
endpoint={webhook.endpoint}
name={webhook.name}
status={webhook.status}
onDelete={() => {
setDeleteData(webhook);
}}
onEdit={() => {
onEditWebhook(webhook.name);
}}
onClick={onClickWebhook}
/>
</div>
))}
{Boolean(!isNil(paging.after) || !isNil(paging.before)) && (
<NextPrevious paging={paging} pagingHandler={onPageChange} />
)}
{deleteData && (
<ConfirmationModal
bodyText={`You want to delete webhook ${deleteData.name} permanently? This action cannot be reverted.`}
cancelText="Discard"
confirmButtonCss="tw-bg-error hover:tw-bg-error focus:tw-bg-error"
confirmText="Delete"
header="Are you sure?"
onCancel={() => setDeleteData(undefined)}
onConfirm={handleDelete}
/>
)}
</div>
</PageLayout>
) : (
<PageLayout>
<ErrorPlaceHolder>
<p className="tw-text-center">No webhooks found</p>
<p className="tw-text-center">
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
<Button
className={classNames('tw-h-8 tw-rounded tw-my-3', {
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
})}
data-testid="add-webhook-button"
size="small"
theme="primary"
variant="contained"
onClick={onAddWebhook}>
Add New Webhook
</Button>
</NonAdminAction>
</p>
</ErrorPlaceHolder>
</PageLayout>
);
};

View File

@ -29,10 +29,11 @@ type EditorContentRef = {
type Props = {
value: string;
readonly?: boolean;
};
const MarkdownWithPreview = forwardRef<editorRef, Props>(
({ value }: Props, ref) => {
({ value, readonly }: Props, ref) => {
const [activeTab, setActiveTab] = useState<number>(1);
const [preview, setPreview] = useState<string>('');
const [initValue, setInitValue] = useState<string>(value ?? '');
@ -106,6 +107,7 @@ const MarkdownWithPreview = forwardRef<editorRef, Props>(
<RichTextEditor
format={isValidJSONString(initValue) ? 'json' : 'markdown'}
initvalue={initValue}
readonly={readonly}
ref={editorRef}
/>
)}

View File

@ -12,11 +12,8 @@
*/
import React, { FunctionComponent } from 'react';
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../../constants/constants';
import { Status } from '../../../generated/entity/events/webhook';
import { stringToHTML } from '../../../utils/StringsUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import NonAdminAction from '../non-admin-action/NonAdminAction';
import WebhookDataCardBody from './WebhookDataCardBody';
type Props = {
@ -24,18 +21,20 @@ type Props = {
description?: string;
endpoint: string;
status?: Status;
onDelete: () => void;
onEdit: () => void;
onClick?: (name: string) => void;
};
const WebhookDataCard: FunctionComponent<Props> = ({
name,
description,
endpoint,
status = Status.NotStarted,
onDelete,
onEdit,
status = Status.Disabled,
onClick,
}: Props) => {
const handleLinkClick = () => {
onClick?.(name);
};
return (
<div
className="tw-bg-white tw-p-3 tw-border tw-border-main tw-rounded-md"
@ -43,36 +42,13 @@ const WebhookDataCard: FunctionComponent<Props> = ({
<div>
<div className="tw-flex tw-items-center">
<h6 className="tw-flex tw-items-center tw-m-0 tw-heading">
<span className="tw-text-grey-body tw-font-medium">
<button
className="tw-text-grey-body tw-font-medium"
data-testid="webhook-link"
onClick={handleLinkClick}>
{stringToHTML(name)}
</span>
</button>
</h6>
<div className="tw-flex tw-flex-auto tw-justify-end">
<NonAdminAction position="top" title={TITLE_FOR_NON_ADMIN_ACTION}>
<button
className="focus:tw-outline-none tw-ml-2"
data-testid={`edit-webhook-${name}`}
onClick={onEdit}>
<SVGIcons
alt="edit"
icon={Icons.EDIT}
title="Edit"
width="12px"
/>
</button>
<button
className="focus:tw-outline-none tw-ml-2"
data-testid={`delete-webhook-${name}`}
onClick={onDelete}>
<SVGIcons
alt="delete"
icon={Icons.DELETE}
title="Delete"
width="12px"
/>
</button>
</NonAdminAction>
</div>
</div>
</div>
<div className="tw-pt-3">

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import classNames from 'classnames';
import { startCase } from 'lodash';
import React, { FunctionComponent } from 'react';
import RichTextEditorPreviewer from '../rich-text-editor/RichTextEditorPreviewer';
@ -28,8 +29,16 @@ const WebhookDataCardBody: FunctionComponent<Props> = ({
}: Props) => {
return (
<div data-testid="card-body">
<div className="tw-mb-3">
<span>{startCase(status)}</span>
<div className="tw-mb-3 tw-flex">
<span className="tw-flex tw-items-center">
<div
className={classNames(
'tw-w-3 tw-h-3 tw-rounded-full',
`tw-bg-${status}`
)}
/>
<span className="tw-ml-1">{startCase(status)}</span>
</span>
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">|</span>
<span>{endpoint}</span>
</div>

View File

@ -20,6 +20,7 @@ const CheckBoxDropDownList = ({
setIsOpen,
onSelect,
selectedItems,
disabledItems,
}: DropDownListProp) => {
const { isAuthDisabled, isAdminUser } = useAuth();
@ -37,11 +38,11 @@ const CheckBoxDropDownList = ({
tw-right-0 tw-w-full tw-mt-1 tw-shadow-lg tw-border tw-border-main
tw-bg-white tw-rounded focus:tw-outline-none"
role="menu">
<div className="py-1" role="none">
<div className="tw-py-1" role="none">
{dropDownList.map((item: DropDownListItem, index: number) =>
!item.isAdminOnly || isAuthDisabled || isAdminUser ? (
<div
className="tw-cursor-pointer"
className="tw-cursor-pointer tw-py-1"
key={index}
onClick={(e) => onSelect && onSelect(e, item.value as string)}>
<input
@ -49,6 +50,9 @@ const CheckBoxDropDownList = ({
selectedItems?.includes(item.value as string)
)}
className="tw-ml-3 tw-mr-2 tw-align-middle custom-checkbox"
disabled={Boolean(
disabledItems?.includes(item.value as string)
)}
type="checkbox"
onChange={() => {
return;

View File

@ -28,6 +28,8 @@ const DropDown: React.FC<DropDownProp> = ({
dropDownList,
onSelect,
selectedItems,
disabledItems,
hiddenItems = [],
isDropDownIconVisible = true,
isLableVisible = true,
}: DropDownProp) => {
@ -50,6 +52,7 @@ const DropDown: React.FC<DropDownProp> = ({
case DropDownType.CHECKBOX:
return (
<CheckBoxDropDownList
disabledItems={disabledItems}
dropDownList={dropDownList}
selectedItems={selectedItems}
setIsOpen={setIsOpen}
@ -92,7 +95,11 @@ const DropDown: React.FC<DropDownProp> = ({
) : (
<span className="tw-flex tw-flex-wrap tw--my-0.5">
{dropDownList.map((item: DropDownListItem) => {
if (selectedItems?.includes(item.value as string)) {
if (
selectedItems
?.filter((item) => !hiddenItems.includes(item))
.includes(item.value as string)
) {
return (
<p
className={classNames(

View File

@ -43,6 +43,8 @@ export type DropDownListProp = {
listGroups?: Array<string>;
searchString?: string;
selectedItems?: Array<string>;
disabledItems?: Array<string>;
hiddenItems?: Array<string>;
showSearchBar?: boolean;
value?: string;
onSelect?: (

View File

@ -0,0 +1,14 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const WILD_CARD_CHAR = '*';

View File

@ -0,0 +1,17 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum FormSubmitType {
ADD = 'add',
EDIT = 'edit',
}

View File

@ -32,6 +32,10 @@ export interface Webhook {
* Description of the application.
*/
description?: string;
/**
* Display Name that identifies this webhook.
*/
displayName?: string;
/**
* When set to `true`, the webhook event notification is enabled. Set it to `false` to
* disable the subscription. (Default `true`).
@ -67,8 +71,8 @@ export interface Webhook {
*/
secretKey?: string;
/**
* Status is `notStarted`, when webhook was created with `enabled` set to false and it never
* started publishing events. Status is `started` when webhook is normally functioning and
* Status is `disabled`, when webhook was created with `enabled` set to false and it never
* started publishing events. Status is `active` when webhook is normally functioning and
* 200 OK response was received for callback notification. Status is `failed` on bad
* callback URL, connection failures, `1xx`, and `3xx` response was received for callback
* notification. Status is `awaitingRetry` when previous attempt at callback timed out or
@ -187,17 +191,17 @@ export interface FailureDetails {
}
/**
* Status is `notStarted`, when webhook was created with `enabled` set to false and it never
* started publishing events. Status is `started` when webhook is normally functioning and
* Status is `disabled`, when webhook was created with `enabled` set to false and it never
* started publishing events. Status is `active` when webhook is normally functioning and
* 200 OK response was received for callback notification. Status is `failed` on bad
* callback URL, connection failures, `1xx`, and `3xx` response was received for callback
* notification. Status is `awaitingRetry` when previous attempt at callback timed out or
* received `4xx`, `5xx` response. Status is `retryLimitReached` after all retries fail.
*/
export enum Status {
Active = 'active',
AwaitingRetry = 'awaitingRetry',
Disabled = 'disabled',
Failed = 'failed',
NotStarted = 'notStarted',
RetryLimitReached = 'retryLimitReached',
Started = 'started',
}

View File

@ -19,10 +19,13 @@ import { addWebhook } from '../../axiosAPIs/webhookAPI';
import AddWebhook from '../../components/AddWebhook/AddWebhook';
import PageContainerV1 from '../../components/containers/PageContainerV1';
import { ROUTES } from '../../constants/constants';
import { FormSubmitType } from '../../enums/form.enum';
import { CreateWebhook } from '../../generated/api/events/createWebhook';
import { useAuth } from '../../hooks/authHooks';
import useToastContext from '../../hooks/useToastContext';
const AddWebhookPage: FunctionComponent = () => {
const { isAuthDisabled, isAdminUser } = useAuth();
const history = useHistory();
const showToast = useToastContext();
const [status, setStatus] = useState<LoadingState>('initial');
@ -57,7 +60,9 @@ const AddWebhookPage: FunctionComponent = () => {
return (
<PageContainerV1>
<AddWebhook
allowAccess={isAdminUser || isAuthDisabled}
header="Add Webhook"
mode={FormSubmitType.ADD}
saveState={status}
onCancel={handleCancel}
onSave={handleSave}

View File

@ -15,22 +15,30 @@ import { AxiosError } from 'axios';
import { LoadingState } from 'Models';
import React, { FunctionComponent, useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { getWebhookByName, updateWebhook } from '../../axiosAPIs/webhookAPI';
import {
deleteWebhook,
getWebhookByName,
updateWebhook,
} from '../../axiosAPIs/webhookAPI';
import AddWebhook from '../../components/AddWebhook/AddWebhook';
import PageContainerV1 from '../../components/containers/PageContainerV1';
import Loader from '../../components/Loader/Loader';
import { ROUTES } from '../../constants/constants';
import { FormSubmitType } from '../../enums/form.enum';
import { CreateWebhook } from '../../generated/api/events/createWebhook';
import { Webhook } from '../../generated/entity/events/webhook';
import { useAuth } from '../../hooks/authHooks';
import useToastContext from '../../hooks/useToastContext';
const EditWebhookPage: FunctionComponent = () => {
const { webhookName } = useParams<{ [key: string]: string }>();
const { isAuthDisabled, isAdminUser } = useAuth();
const history = useHistory();
const showToast = useToastContext();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [webhookData, setWebhookData] = useState<Webhook>();
const [status, setStatus] = useState<LoadingState>('initial');
const [deleteStatus, setDeleteStatus] = useState<LoadingState>('initial');
const fetchWebhook = () => {
setIsLoading(true);
@ -75,6 +83,22 @@ const EditWebhookPage: FunctionComponent = () => {
});
};
const handleDelete = (id: string) => {
setDeleteStatus('waiting');
deleteWebhook(id)
.then(() => {
setDeleteStatus('initial');
goToWebhooks();
})
.catch((err: AxiosError) => {
showToast({
variant: 'error',
body: err.message || 'Something went wrong!',
});
setDeleteStatus('initial');
});
};
useEffect(() => {
fetchWebhook();
}, []);
@ -83,10 +107,14 @@ const EditWebhookPage: FunctionComponent = () => {
<PageContainerV1>
{!isLoading ? (
<AddWebhook
allowAccess={isAdminUser || isAuthDisabled}
data={webhookData}
deleteState={deleteStatus}
header="Edit Webhook"
mode={FormSubmitType.EDIT}
saveState={status}
onCancel={handleCancel}
onDelete={handleDelete}
onSave={handleSave}
/>
) : (

View File

@ -15,7 +15,7 @@ import { AxiosError } from 'axios';
import { Paging } from 'Models';
import React, { FunctionComponent, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { deleteWebhook, getWebhooks } from '../../axiosAPIs/webhookAPI';
import { getWebhooks } from '../../axiosAPIs/webhookAPI';
import PageContainerV1 from '../../components/containers/PageContainerV1';
import Loader from '../../components/Loader/Loader';
import Webhooks from '../../components/Webhooks/Webhooks';
@ -68,24 +68,10 @@ const WebhooksPage: FunctionComponent = () => {
history.push(ROUTES.ADD_WEBHOOK);
};
const handleEditWebhook = (name: string) => {
const handleClickWebhook = (name: string) => {
history.push(getEditWebhookPath(name));
};
const handleDeleteWebhook = (id: string) => {
setIsLoading(true);
deleteWebhook(id)
.then(() => {
fetchData();
})
.catch((err: AxiosError) => {
showToast({
variant: 'error',
body: err.message || 'Something went wrong!',
});
});
};
useEffect(() => {
fetchData();
}, []);
@ -97,8 +83,7 @@ const WebhooksPage: FunctionComponent = () => {
data={data}
paging={paging}
onAddWebhook={handleAddWebhook}
onDeleteWebhook={handleDeleteWebhook}
onEditWebhook={handleEditWebhook}
onClickWebhook={handleClickWebhook}
onPageChange={handlePageChange}
/>
) : (

View File

@ -91,10 +91,10 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
/>
<Route exact component={EntityVersionPage} path={ROUTES.ENTITY_VERSION} />
<Route exact component={WebhooksPage} path={ROUTES.WEBHOOKS} />
<Route exact component={AddWebhookPage} path={ROUTES.ADD_WEBHOOK} />
<Route exact component={EditWebhookPage} path={ROUTES.EDIT_WEBHOOK} />
{isAuthDisabled || isAdminUser ? (
<>
<Route exact component={AddWebhookPage} path={ROUTES.ADD_WEBHOOK} />
<Route exact component={RolesPage} path={ROUTES.ROLES} />
<Route exact component={UserListPage} path={ROUTES.USER_LIST} />
</>

View File

@ -182,6 +182,9 @@
@apply tw-w-full tw-text-grey-body tw-rounded tw-border tw-border-main
focus:tw-outline-none focus:tw-border-focus hover:tw-border-hover;
}
input:disabled {
@apply tw-cursor-not-allowed;
}
/* Dropdown CSS start */
.dropdown-list {

View File

@ -121,12 +121,12 @@ module.exports = {
notStarted: ideal,
started: success,
failed: error,
awaitingRetry: info,
awaitingRetry: success,
retryLimitReached: warning,
'notStarted-lite': idealBG,
'started-lite': successBG,
'failed-lite': errorBG,
'awaitingRetry-lite': infoBG,
'awaitingRetry-lite': successBG,
'retryLimitReached-lite': warningBG,
// Webhook statuses end
separator: mainSeparator,