From 00e6996ac093001245bbd00bbe14df010bae0282 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Sat, 3 Sep 2022 09:12:55 +0530 Subject: [PATCH] fix(ui): webhook event handling with tree structure (#7164) * fix(ui): webhook event handling with tree structure * Fix layout * minor fix * Fix edit webhook redirection. * Fix tree structure issues Co-authored-by: Sachin Chaurasiya --- .../ui/src/axiosAPIs/eventFiltersAPI.ts | 8 + .../src/components/AddWebhook/AddWebhook.tsx | 43 ++---- .../EventFilterSelect.component.tsx | 89 ----------- .../AddWebhook/EventFilterTree.component.tsx | 138 ++++++++++++++++++ .../ui/src/components/Webhooks/WebhooksV1.tsx | 105 ++++++------- .../src/main/resources/ui/src/jsons/en.ts | 1 + .../ActivityFeedSettingsPage.test.tsx | 4 +- .../ActivityFeedSettingsPage.tsx | 101 +++++++------ .../ActivityFeedSettingsPage.utils.ts | 23 ++- .../EditWebhookPage.component.tsx | 27 +++- 10 files changed, 300 insertions(+), 239 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/EventFilterSelect.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/EventFilterTree.component.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/eventFiltersAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/eventFiltersAPI.ts index 9c4fe8de2b5..fa50defd27e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/eventFiltersAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/eventFiltersAPI.ts @@ -68,3 +68,11 @@ export const resetAllFilters = async () => { return response.data; }; + +export const getInitialFilters = async () => { + const url = `${BASE_URL}/bootstrappedFilters`; + + const response = await axiosClient.get(url); + + return response.data; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/AddWebhook.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/AddWebhook.tsx index 87f2bddeff4..36494583ed2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/AddWebhook.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/AddWebhook.tsx @@ -14,7 +14,6 @@ import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from 'antd'; -import { Store } from 'antd/lib/form/interface'; import classNames from 'classnames'; import cryptoRandomString from 'crypto-random-string-with-promisify-polyfill'; import { cloneDeep, isEmpty, isNil } from 'lodash'; @@ -26,7 +25,7 @@ import React, { useRef, useState, } from 'react'; -import { ROUTES, TERM_ALL } from '../../constants/constants'; +import { ROUTES } from '../../constants/constants'; import { GlobalSettingOptions, GlobalSettingsMenuCategory, @@ -55,7 +54,6 @@ import { import { checkPermission } from '../../utils/PermissionsUtils'; import { getSettingPath } from '../../utils/RouterUtils'; import SVGIcons, { Icons } from '../../utils/SvgUtils'; -import { getEventFilters } from '../../utils/WebhookUtils'; import { Button } from '../buttons/Button/Button'; import CopyToClipboardButton from '../buttons/CopyToClipboardButton/CopyToClipboardButton'; import CardV1 from '../common/Card/CardV1'; @@ -67,8 +65,7 @@ import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal'; import { usePermissionProvider } from '../PermissionProvider/PermissionProvider'; import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface'; import { AddWebhookProps } from './AddWebhook.interface'; -import EventFilterSelect from './EventFilterSelect.component'; -import { EVENT_FILTER_FORM_INITIAL_VALUE } from './WebhookConstants'; +import EventFilterTree from './EventFilterTree.component'; const CONFIGURE_TEXT: { [key: string]: string } = { msteams: CONFIGURE_MS_TEAMS_TEXT, @@ -80,26 +77,6 @@ const Field = ({ children }: { children: React.ReactNode }) => { return
{children}
; }; -const getFormData = (eventFilters: EventFilter[]): Store => { - if (eventFilters.length === 1 && eventFilters[0].entityType === TERM_ALL) { - return EVENT_FILTER_FORM_INITIAL_VALUE; - } - - const formEventFilters = {} as Store; - - eventFilters?.forEach((eventFilter) => { - if (eventFilter.entityType === TERM_ALL) { - return; - } - - formEventFilters[eventFilter.entityType] = true; - formEventFilters[`${eventFilter.entityType}-tree`] = - eventFilter.filters?.map((filter) => filter.eventType) || []; - }); - - return formEventFilters; -}; - const AddWebhook: FunctionComponent = ({ data, header, @@ -113,11 +90,9 @@ const AddWebhook: FunctionComponent = ({ onSave, }: AddWebhookProps) => { const markdownRef = useRef(); - const [eventFilterFormData, setEventFilterFormData] = useState( - data?.eventFilters - ? getFormData(data?.eventFilters) - : EVENT_FILTER_FORM_INITIAL_VALUE - ); + const [eventFilterFormData, setEventFilterFormData] = useState< + EventFilter[] | undefined + >(data?.eventFilters); const [name, setName] = useState(data?.name || ''); const [endpointUrl, setEndpointUrl] = useState(data?.endpoint || ''); const [description] = useState(data?.description || ''); @@ -250,7 +225,7 @@ const AddWebhook: FunctionComponent = ({ name, description: markdownRef.current?.getEditorContent() || undefined, endpoint: endpointUrl, - eventFilters: getEventFilters(eventFilterFormData), + eventFilters: eventFilterFormData ?? ([] as EventFilter[]), batchSize, timeout: connectionTimeout, enabled: active, @@ -468,9 +443,9 @@ const AddWebhook: FunctionComponent = ({ , 'tw-mt-3' )} - setEventFilterFormData(data)} +
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/EventFilterSelect.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/EventFilterSelect.component.tsx deleted file mode 100644 index 31fd48115b0..00000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/EventFilterSelect.component.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Col, Form, Row, TreeSelect } from 'antd'; -import Checkbox from 'antd/lib/checkbox/Checkbox'; -import { Store } from 'antd/lib/form/interface'; -import { startCase } from 'lodash'; -import React, { useMemo } from 'react'; -import { - EventFilter, - EventType, -} from '../../generated/api/events/createWebhook'; -import { Entities } from './WebhookConstants'; - -export enum EventUpdateTypes { - UpdatedFollowers = 'updatedFollowers', - UpdatedTags = 'updatedTags', - UpdatedOwner = 'updatedOwner', - UpdateDescription = 'updateDescription', -} - -interface EventFilterSelectProps { - eventFilterFormData: Store; - setEventFilterFormData: (formData: EventFilter[]) => void; -} -const EventFilterSelect = ({ - eventFilterFormData, - setEventFilterFormData, -}: EventFilterSelectProps) => { - const metricsOptions = useMemo( - () => [ - { - title: 'All', - value: 'all', - key: 'all', - children: Object.values(EventType).map((metric) => ({ - title: startCase(metric), - value: metric, - key: metric, - children: - metric === EventType.EntityUpdated - ? Object.values(EventUpdateTypes).map((updateType) => ({ - title: startCase(updateType), - value: `${EventType.EntityUpdated}-${updateType}`, - key: `${EventType.EntityUpdated}-${updateType}`, - })) - : undefined, - })), - }, - ], - [] - ); - - return ( -
{ - setEventFilterFormData(data); - }}> - - {Object.keys(Entities).map((key) => { - const value = Entities[key]; - - return ( - - - {value} - - - - - - ); - })} - -
- ); -}; - -export default EventFilterSelect; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/EventFilterTree.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/EventFilterTree.component.tsx new file mode 100644 index 00000000000..a8c31f8e01d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/EventFilterTree.component.tsx @@ -0,0 +1,138 @@ +import { Divider, Tree } from 'antd'; +import { cloneDeep, isEmpty, map, startCase } from 'lodash'; +import React, { Key, useEffect, useMemo, useState } from 'react'; +import { getInitialFilters } from '../../axiosAPIs/eventFiltersAPI'; +import { TERM_ALL } from '../../constants/constants'; +import { EventFilter } from '../../generated/api/events/createWebhook'; +import { Filters } from '../../generated/settings/settings'; +import { getEventFilterFromTree } from '../../pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.utils'; +import './../../pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.style.less'; + +interface EventFilterTreeProps { + value: EventFilter[]; + onChange: (data: EventFilter[]) => void; +} + +const EventFilterTree = ({ value, onChange }: EventFilterTreeProps) => { + const [updatedTree, setUpdatedTree] = useState>(); + const [initialFilters, setInitialFilters] = useState([]); + + const eventFilters = isEmpty(value) ? initialFilters : value; + const fetchInitialFilters = async () => { + try { + const data = await getInitialFilters(); + setInitialFilters(data); + isEmpty(value) && onChange(getEventFilterFromTree({}, eventFilters)); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`filed to fetch event filters `); + } + }; + + useEffect(() => { + fetchInitialFilters(); + }, []); + + const generateTreeData = (entityType: string, data?: Filters[]) => { + return [ + { + key: entityType, + title: {startCase(entityType)}, + data: true, + children: + data?.map(({ eventType, include, exclude }) => { + const key = `${entityType}-${eventType}` as string; + + return { + key: key, + title: startCase(eventType), + data: isEmpty(value) ? true : include, + children: + (include?.length === 1 && include[0] === TERM_ALL) || + (exclude?.length === 1 && exclude[0] === TERM_ALL) + ? undefined + : [ + ...(include?.map((inc) => ({ + key: `${key}-${inc}`, + title: startCase(inc), + data: true, + })) || []), + ...(exclude?.map((ex) => ({ + key: `${key}-${ex}`, + title: startCase(ex), + data: false, + })) || []), + ], + }; + }) || [], + }, + ]; + }; + + const handleTreeCheckChange = (keys: Key[], entityType: string) => { + const updateData = cloneDeep(updatedTree || {}); + + updateData[entityType] = keys as string[]; + + onChange(getEventFilterFromTree(cloneDeep(updateData), eventFilters)); + setUpdatedTree(updateData); + }; + const getCheckedKeys = (eventFilters: EventFilter[]) => { + const checkedArray = [] as string[]; + const clonedFilters = cloneDeep(eventFilters); + + clonedFilters?.map(({ entityType, filters }) => { + filters && + filters.map((obj) => { + if ( + obj.include && + obj.include.length === 1 && + obj.include[0] === 'all' + ) { + checkedArray.push(`${entityType}-${obj.eventType}`); + } else { + obj?.include?.forEach((entityUpdated) => { + const name = `${entityType}-${obj.eventType}-${entityUpdated}`; + checkedArray.push(name); + }); + } + }); + }); + + return checkedArray; + }; + + const checkedKeys = useMemo(() => { + const checkKeys = getCheckedKeys(eventFilters as EventFilter[]); + + return checkKeys; + }, [eventFilters, updatedTree]); + + return ( + <> + {initialFilters && + map(initialFilters, ({ entityType, filters }, index) => ( + <> + {entityType !== TERM_ALL ? ( +
+ + handleTreeCheckChange(keys as Key[], entityType) + } + /> + {index !== initialFilters.length - 1 && } +
+ ) : null} + + ))} + + ); +}; + +export default EventFilterTree; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Webhooks/WebhooksV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Webhooks/WebhooksV1.tsx index 375eb8a7f7c..f04d45326cb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Webhooks/WebhooksV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Webhooks/WebhooksV1.tsx @@ -129,64 +129,53 @@ const WebhooksV1: FC = ({ return ( <> - - - - + + + + + + + + + + setWebhook(data)} + onEdit={onClickWebhook} + /> + {Boolean(!isNil(paging.after) || !isNil(paging.before)) && ( + + )} {selectedWebhook && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts b/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts index 27b426e4f24..fc138ce71e8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts +++ b/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts @@ -169,6 +169,7 @@ const jsonData = { 'update-profile-congif-success': 'Profile config updated successfully!', 'update-test-case-success': 'Test case updated successfully!', + 'update-webhook-success': 'Webhook updated successfully!', }, 'form-error-messages': { 'empty-email': 'Email is required.', diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.test.tsx index a79d1e6ac09..6378cff132d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.test.tsx @@ -6,6 +6,8 @@ describe('Test ActivityFeedSettingsPage', () => { it('should render properly', async () => { const { findByText } = render(); - expect(await findByText(/Activity Feed/)).toBeInTheDocument(); + expect( + await findByText(/No activity feed settings available/) + ).toBeInTheDocument(); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.tsx index c2f50bb26d5..dd717b8875c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.tsx @@ -9,6 +9,7 @@ import { resetAllFilters, updateFilters, } from '../../axiosAPIs/eventFiltersAPI'; +import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder'; import Loader from '../../components/Loader/Loader'; import { TERM_ALL } from '../../constants/constants'; import { @@ -187,51 +188,61 @@ const ActivityFeedSettingsPage: React.FC = () => { ) : ( - - - - Activity Feed - - - - - {eventFilters && - map(eventFilters, ({ entityType, filters }, index) => ( - <> - {entityType !== TERM_ALL ? ( -
- - handleTreeCheckChange(keys as Key[], entityType) - } - /> - {index !== eventFilters?.length - 1 && } -
- ) : null} - - ))} -
- - - - - - - - - -
+ <> + {eventFilters ? ( + <> + + + + Activity Feed + + + + + {eventFilters && + map(eventFilters, ({ entityType, filters }, index) => ( + <> + {entityType !== TERM_ALL ? ( +
+ + handleTreeCheckChange(keys as Key[], entityType) + } + /> + {index !== eventFilters?.length - 1 && } +
+ ) : null} + + ))} +
+ + + + + + + + + +
+ + ) : ( + + No activity feed settings available + + )} + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.utils.ts b/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.utils.ts index 745697c1afc..3b2dc2aad41 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.utils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.utils.ts @@ -1,4 +1,4 @@ -import { intersection, isEmpty, isUndefined, xor } from 'lodash'; +import { isEmpty, isUndefined, xor } from 'lodash'; import { EventFilter, EventType, @@ -77,19 +77,30 @@ export const getEventFilterFromTree = ( filters: eventFilter.filters?.map((filter) => { let includeList = filter.include; let excludeList = filter.exclude; + + // derive the merge list + const mergedList = [ + ...(includeList as string[]), + ...(excludeList as string[]), + ]; + + // manipulate tree if event type is present if (updatedTree[eventFilter.entityType]) { + // Split the value to get list of [eventType, filter, event] const temp = updatedTree[eventFilter.entityType].map((key) => key.split('-') ); + // grab the list of current eventType const eventList = temp.filter((f) => f[1] === filter.eventType); + if (eventList.length > 0) { if (filter.eventType === EventType.EntityUpdated) { - includeList = intersection( - filter.include ?? [], - eventList.map((f) => f[2]) - ); - excludeList = xor(filter.include, includeList); + // derive include list based on selected events + includeList = eventList.map((f) => f[2]).filter(Boolean); + + // derive the exclude list by symmetric difference + excludeList = xor(mergedList, includeList); } else { includeList = ['all']; excludeList = []; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/EditWebhookPage/EditWebhookPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/EditWebhookPage/EditWebhookPage.component.tsx index 60e17dc1b64..4e5f7247399 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/EditWebhookPage/EditWebhookPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/EditWebhookPage/EditWebhookPage.component.tsx @@ -30,11 +30,11 @@ import { } from '../../constants/globalSettings.constants'; import { FormSubmitType } from '../../enums/form.enum'; import { CreateWebhook } from '../../generated/api/events/createWebhook'; -import { Webhook } from '../../generated/entity/events/webhook'; +import { Webhook, WebhookType } from '../../generated/entity/events/webhook'; import { useAuth } from '../../hooks/authHooks'; import jsonData from '../../jsons/en'; import { getSettingPath } from '../../utils/RouterUtils'; -import { showErrorToast } from '../../utils/ToastUtils'; +import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; const EDIT_HEADER_WEBHOOKS_TITLE: { [key: string]: string } = { msteams: 'MS Teams', @@ -69,11 +69,23 @@ const EditWebhookPage: FunctionComponent = () => { }; const goToWebhooks = () => { + let type = GlobalSettingOptions.WEBHOOK; + switch (webhookData?.webhookType) { + case WebhookType.Msteams: + type = GlobalSettingOptions.MSTEAMS; + + break; + case WebhookType.Slack: + type = GlobalSettingOptions.SLACK; + + break; + + default: + break; + } + history.push( - getSettingPath( - GlobalSettingsMenuCategory.INTEGRATIONS, - GlobalSettingOptions.WEBHOOK - ) + `${getSettingPath(GlobalSettingsMenuCategory.INTEGRATIONS, type)}` ); }; @@ -92,6 +104,9 @@ const EditWebhookPage: FunctionComponent = () => { setStatus('initial'); goToWebhooks(); }, 500); + showSuccessToast( + jsonData['api-success-messages']['update-webhook-success'] + ); } else { throw jsonData['api-error-messages']['unexpected-error']; }