From 52b100fbc6b006c99393a80da6782cc3fce782a0 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:01:39 +0530 Subject: [PATCH] fix(ui): instead ui render list based on api response (#7132) * fix(ui): instead ui render list based on api response * update ui * add support for resetFilters * update api * update logic * update activity feed settings page --- .../resources/settings/SettingsResource.java | 43 +++ .../ui/src/axiosAPIs/eventFiltersAPI.ts | 39 ++- .../ActivityFeedSettingsPage.style.less | 19 +- .../ActivityFeedSettingsPage.tsx | 255 ++++++++---------- .../ActivityFeedSettingsPage.utils.ts | 69 ++++- 5 files changed, 258 insertions(+), 167 deletions(-) diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/resources/settings/SettingsResource.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/resources/settings/SettingsResource.java index c8f9e5b6140..a555dffe83d 100644 --- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/resources/settings/SettingsResource.java +++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/resources/settings/SettingsResource.java @@ -32,6 +32,7 @@ import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PATCH; +import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -43,6 +44,7 @@ import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriInfo; import lombok.extern.slf4j.Slf4j; import org.openmetadata.catalog.CatalogApplicationConfig; +import org.openmetadata.catalog.filter.EventFilter; import org.openmetadata.catalog.filter.FilterRegistry; import org.openmetadata.catalog.filter.Filters; import org.openmetadata.catalog.jdbi3.CollectionDAO; @@ -65,6 +67,7 @@ import org.openmetadata.common.utils.CommonUtil; public class SettingsResource { private final SettingsRepository settingsRepository; private final Authorizer authorizer; + private List bootStrappedFilters; @SuppressWarnings("unused") // Method used for reflection public void initialize(CatalogApplicationConfig config) throws IOException { @@ -84,6 +87,9 @@ public class SettingsResource { settings.forEach( (setting) -> { try { + if (setting.getConfigType() == ACTIVITY_FEED_FILTER_SETTING) { + bootStrappedFilters = FilterUtil.getEventFilterFromSettings(setting); + } Settings storedSettings = settingsRepository.getConfigWithKey(setting.getConfigType().toString()); if (storedSettings == null) { // Only in case a config doesn't exist in DB we insert it @@ -138,6 +144,43 @@ public class SettingsResource { return settingsRepository.listAllConfigs(); } + @GET + @Path("/bootstrappedFilters") + @Operation( + operationId = "listBootstrappedFilter", + summary = "List All BootStrapped Filters", + tags = "settings", + description = "Get a List of all OpenMetadata Bootstrapped Filters", + responses = { + @ApiResponse( + responseCode = "200", + description = "List of Settings", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = SettingsList.class))) + }) + public List getBootstrapFilters(@Context UriInfo uriInfo, @Context SecurityContext securityContext) + throws IOException { + return bootStrappedFilters; + } + + @POST + @Path("/resetFilters") + @Operation( + operationId = "resetFilters", + summary = "Reset filters to initial state", + tags = "settings", + description = "Reset filters to it's initial state", + responses = { + @ApiResponse( + responseCode = "200", + description = "List of Filters", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = SettingsList.class))) + }) + public Response resetFilters(@Context UriInfo uriInfo, @Context SecurityContext securityContext) throws IOException { + Settings settings = + new Settings().withConfigType(ACTIVITY_FEED_FILTER_SETTING).withConfigValue(bootStrappedFilters); + return settingsRepository.createNewSetting(settings); + } + @GET @Path("/{settingName}") @Operation( 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 f75c18113b2..9c4fe8de2b5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/eventFiltersAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/eventFiltersAPI.ts @@ -17,11 +17,15 @@ import { EventFilter, Filters } from '../generated/settings/settings'; const BASE_URL = '/settings'; +export interface ActivityFeedSettings { + config_type: string; + config_value: EventFilter[]; +} + export const getActivityFeedEventFilters = async () => { - const response = await axiosClient.get<{ - config_type: string; - config_value: EventFilter[]; - }>(`${BASE_URL}/activityFeedFilterSetting`); + const response = await axiosClient.get( + `${BASE_URL}/activityFeedFilterSetting` + ); return response.data.config_value; }; @@ -37,11 +41,30 @@ export const createOrUpdateActivityFeedEventFilter = async ( const response = await axiosClient.put< Filters[], - AxiosResponse<{ - config_type: string; - config_value: EventFilter[]; - }> + AxiosResponse >(url, payload, configOptions); return response.data.config_value; }; + +export const updateFilters = async (data: ActivityFeedSettings) => { + const url = `${BASE_URL}`; + + const response = await axiosClient.put< + ActivityFeedSettings, + AxiosResponse + >(url, data); + + return response.data; +}; + +export const resetAllFilters = async () => { + const url = `${BASE_URL}/resetFilters`; + + const response = await axiosClient.post< + null, + AxiosResponse + >(url); + + return response.data; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.style.less b/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.style.less index 3098858e6ce..67ed532ae68 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.style.less +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ActivityFeedSettingsPage/ActivityFeedSettingsPage.style.less @@ -1,7 +1,16 @@ -.ant-collapse > .ant-collapse-item > .ant-collapse-header { - align-items: center; -} +.activity-feed-settings-tree { + .ant-tree-list-holder-inner { + margin-left: -16px; + } + .ant-tree-list-holder { + padding: 0.5rem; + } + .ant-tree-switcher.ant-tree-switcher_open { + visibility: hidden; + } -.arrow { - margin-right: 0.4rem; + .ant-tree-node-content-wrapper { + padding: 0; + } + margin-left: -8px; } 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 7c3a2e0cf94..c2f50bb26d5 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 @@ -1,36 +1,29 @@ -import { Button, Col, Collapse, Row, Space, Tree, Typography } from 'antd'; +/* eslint-disable @typescript-eslint/camelcase */ +import { Button, Card, Col, Divider, Row, Space, Tree, Typography } from 'antd'; import { AxiosError } from 'axios'; -import { - cloneDeep, - isArray, - isEmpty, - isUndefined, - map, - startCase, -} from 'lodash'; +import { cloneDeep, isUndefined, map, startCase } from 'lodash'; import React, { Key, useEffect, useState } from 'react'; -import { ReactComponent as DownArrow } from '../../assets/svg/down-arrow.svg'; -import { ReactComponent as RightArrow } from '../../assets/svg/right-arrow.svg'; import { - createOrUpdateActivityFeedEventFilter, + ActivityFeedSettings, getActivityFeedEventFilters, + resetAllFilters, + updateFilters, } from '../../axiosAPIs/eventFiltersAPI'; import Loader from '../../components/Loader/Loader'; import { TERM_ALL } from '../../constants/constants'; -import { EventFilter, Filters } from '../../generated/settings/settings'; +import { + EventFilter, + Filters, + SettingType, +} from '../../generated/settings/settings'; import jsonData from '../../jsons/en'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; -import { - ActivityFeedEntity, - formData, -} from './ActivityFeedSettingsPage.constants'; import './ActivityFeedSettingsPage.style.less'; -import { getPayloadFromSelected } from './ActivityFeedSettingsPage.utils'; +import { getEventFilterFromTree } from './ActivityFeedSettingsPage.utils'; const ActivityFeedSettingsPage: React.FC = () => { const [eventFilters, setEventFilters] = useState(); const [loading, setLoading] = useState(true); - const [selectedKeys, setSelectedKeys] = useState([]); const [selectedKey, setSelectedKey] = useState(); const [checkedKeys, setCheckedKeys] = useState([]); const [updatedTree, setUpdatedTree] = useState>(); @@ -52,17 +45,11 @@ const ActivityFeedSettingsPage: React.FC = () => { } }; - const createActivityFeed = async ( - entityName: string, - selectedData: Filters[] - ) => { + const createActivityFeed = async (req: ActivityFeedSettings) => { try { setLoading(true); - const data = await createOrUpdateActivityFeedEventFilter( - entityName, - selectedData - ); - const filteredData = data?.filter( + const data = await updateFilters(req); + const filteredData = data.config_value?.filter( ({ entityType }) => entityType !== TERM_ALL ); @@ -77,52 +64,40 @@ const ActivityFeedSettingsPage: React.FC = () => { } }; - const handleExpandStateChange = (keys: string | string[]) => { - setSelectedKeys(keys); - const key = [...keys]; - - setSelectedKey(key[key.length - 1]); - }; - - const handleExpandAll = () => { - if (isArray(selectedKeys) && selectedKeys.length === eventFilters?.length) { - setSelectedKeys([]); - } else { - setSelectedKeys(eventFilters?.map((e) => e.entityType) || []); - } - }; - const generateTreeData = (entityType: string, data?: Filters[]) => { - return data?.map(({ eventType, include }) => { - const key = `${entityType}-${eventType}` as string; - - return { - key: key, - title: startCase(eventType), - data: include, + return [ + { + key: entityType, + title: {startCase(entityType)}, + data: true, children: - eventType === 'entityUpdated' - ? [ - { - key: `${key}-owner`, - title: 'Owner', - }, - { - key: `${key}-description`, - title: 'Description', - }, - { - key: `${key}-tags`, - title: 'Tags', - }, - { - key: `${key}-followers`, - title: 'Followers', - }, - ] - : undefined, - }; - }); + data?.map(({ eventType, include, exclude }) => { + const key = `${entityType}-${eventType}` as string; + + return { + key: key, + title: startCase(eventType), + data: 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 getCheckedKeys = (eventFilters: EventFilter[]) => { @@ -164,15 +139,15 @@ const ActivityFeedSettingsPage: React.FC = () => { const onSave = (event: React.MouseEvent) => { event.stopPropagation(); - if (!isUndefined(updatedTree) && selectedKey) { + if (!isUndefined(updatedTree) && selectedKey && eventFilters) { const deepClonedTree = cloneDeep(updatedTree); - const selectedTree = { - [selectedKey]: deepClonedTree[selectedKey], - }; - const value = getPayloadFromSelected(selectedTree, selectedKey); + const data = { + config_type: SettingType.ActivityFeedFilterSetting, + config_value: getEventFilterFromTree(deepClonedTree, eventFilters), + } as ActivityFeedSettings; - createActivityFeed(selectedKey, value as Filters[]); + createActivityFeed(data); setUpdatedTree(undefined); setSelectedKey(undefined); } @@ -186,7 +161,26 @@ const ActivityFeedSettingsPage: React.FC = () => { const checkKeys = getCheckedKeys(eventFilters as EventFilter[]); setCheckedKeys(checkKeys); - }, [eventFilters, selectedKeys, updatedTree, selectedKey]); + }, [eventFilters, updatedTree, selectedKey]); + + const handleResetClick = async () => { + try { + setLoading(true); + const data = await resetAllFilters(); + const filteredData = data.config_value?.filter( + ({ entityType }) => entityType !== TERM_ALL + ); + + setEventFilters(filteredData); + showSuccessToast( + jsonData['api-success-messages']['add-settings-success'] + ); + } catch { + showErrorToast(jsonData['api-error-messages']['add-settings-error']); + } finally { + setLoading(false); + } + }; return loading ? ( @@ -195,77 +189,48 @@ const ActivityFeedSettingsPage: React.FC = () => { ) : ( - - - Activity Feed - - - {selectedKeys.length === eventFilters?.length - ? 'Collapse All' - : 'Expand All'} - - + + Activity Feed + - { - return ( + + {eventFilters && + map(eventFilters, ({ entityType, filters }, index) => ( <> - {isActive ? ( - - {' '} - - ) : ( - - - - )} - - ); - }} - onChange={handleExpandStateChange}> - {map(eventFilters, ({ entityType }) => ( - <> - {entityType !== TERM_ALL ? ( - + + handleTreeCheckChange(keys as Key[], entityType) } - type="primary" - onClick={(event) => onSave(event)}> - Save - - } - header={ - - - {ActivityFeedEntity[entityType]} - - - } - key={entityType}> - - handleTreeCheckChange(keys as Key[], entityType) - } - /> - - ) : null} - - ))} - + /> + {index !== eventFilters?.length - 1 && } + + ) : null} + + ))} + + + + + + + + + ); }; 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 526fc710a9f..745697c1afc 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,17 +1,24 @@ -import { isEmpty, isUndefined } from 'lodash'; -import { Filters } from '../../generated/settings/settings'; +import { intersection, isEmpty, isUndefined, xor } from 'lodash'; +import { + EventFilter, + EventType, + Filters, +} from '../../generated/settings/settings'; import { getDiffArray } from '../../utils/CommonUtils'; -const entityUpdatedFields = ['description', 'owner', 'tags', 'followers']; - export const getPayloadFromSelected = ( selectedOptions: Record, - selectedKey?: string + selectedKey: string, + selectedEntityEventUpdatedFields: string[] ): void | Array => { const nonUpdatedFields = [] as string[]; const resultArr = []; - if (isUndefined(selectedOptions) || isEmpty(selectedKey)) { + if ( + isUndefined(selectedOptions) && + isEmpty(selectedKey) && + selectedEntityEventUpdatedFields + ) { return [] as Filters[]; } @@ -23,7 +30,7 @@ export const getPayloadFromSelected = ( value.reduce((valueAcc: any, name: string) => { const selected = name.split('-'); - if (selected[1] !== 'entityUpdated') { + if (selected[1] !== EventType.EntityUpdated) { return [ ...valueAcc, { @@ -48,12 +55,56 @@ export const getPayloadFromSelected = ( ); resultArr.push({ - eventType: 'entityUpdated', + eventType: EventType.EntityUpdated, include: selectedUpdatedData, - exclude: getDiffArray(entityUpdatedFields, selectedUpdatedData), + exclude: getDiffArray( + selectedEntityEventUpdatedFields, + selectedUpdatedData + ), }); } return resultArr as Filters[]; } }; + +export const getEventFilterFromTree = ( + updatedTree: Record, + eventFilters: EventFilter[] +): EventFilter[] => { + return eventFilters.map((eventFilter) => ({ + ...eventFilter, + filters: eventFilter.filters?.map((filter) => { + let includeList = filter.include; + let excludeList = filter.exclude; + if (updatedTree[eventFilter.entityType]) { + const temp = updatedTree[eventFilter.entityType].map((key) => + key.split('-') + ); + + 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); + } else { + includeList = ['all']; + excludeList = []; + } + } else { + excludeList = [...(includeList ?? []), ...(excludeList ?? [])]; + includeList = []; + } + } + + return { + ...filter, + include: includeList, + exclude: excludeList, + }; + }), + })); +};