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
This commit is contained in:
Chirag Madlani 2022-09-02 18:01:39 +05:30 committed by GitHub
parent 106ba985b3
commit 52b100fbc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 258 additions and 167 deletions

View File

@ -32,6 +32,7 @@ import javax.validation.Valid;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.PATCH; import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
@ -43,6 +44,7 @@ import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.openmetadata.catalog.CatalogApplicationConfig; import org.openmetadata.catalog.CatalogApplicationConfig;
import org.openmetadata.catalog.filter.EventFilter;
import org.openmetadata.catalog.filter.FilterRegistry; import org.openmetadata.catalog.filter.FilterRegistry;
import org.openmetadata.catalog.filter.Filters; import org.openmetadata.catalog.filter.Filters;
import org.openmetadata.catalog.jdbi3.CollectionDAO; import org.openmetadata.catalog.jdbi3.CollectionDAO;
@ -65,6 +67,7 @@ import org.openmetadata.common.utils.CommonUtil;
public class SettingsResource { public class SettingsResource {
private final SettingsRepository settingsRepository; private final SettingsRepository settingsRepository;
private final Authorizer authorizer; private final Authorizer authorizer;
private List<EventFilter> bootStrappedFilters;
@SuppressWarnings("unused") // Method used for reflection @SuppressWarnings("unused") // Method used for reflection
public void initialize(CatalogApplicationConfig config) throws IOException { public void initialize(CatalogApplicationConfig config) throws IOException {
@ -84,6 +87,9 @@ public class SettingsResource {
settings.forEach( settings.forEach(
(setting) -> { (setting) -> {
try { try {
if (setting.getConfigType() == ACTIVITY_FEED_FILTER_SETTING) {
bootStrappedFilters = FilterUtil.getEventFilterFromSettings(setting);
}
Settings storedSettings = settingsRepository.getConfigWithKey(setting.getConfigType().toString()); Settings storedSettings = settingsRepository.getConfigWithKey(setting.getConfigType().toString());
if (storedSettings == null) { if (storedSettings == null) {
// Only in case a config doesn't exist in DB we insert it // Only in case a config doesn't exist in DB we insert it
@ -138,6 +144,43 @@ public class SettingsResource {
return settingsRepository.listAllConfigs(); 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<EventFilter> 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 @GET
@Path("/{settingName}") @Path("/{settingName}")
@Operation( @Operation(

View File

@ -17,11 +17,15 @@ import { EventFilter, Filters } from '../generated/settings/settings';
const BASE_URL = '/settings'; const BASE_URL = '/settings';
export const getActivityFeedEventFilters = async () => { export interface ActivityFeedSettings {
const response = await axiosClient.get<{
config_type: string; config_type: string;
config_value: EventFilter[]; config_value: EventFilter[];
}>(`${BASE_URL}/activityFeedFilterSetting`); }
export const getActivityFeedEventFilters = async () => {
const response = await axiosClient.get<ActivityFeedSettings>(
`${BASE_URL}/activityFeedFilterSetting`
);
return response.data.config_value; return response.data.config_value;
}; };
@ -37,11 +41,30 @@ export const createOrUpdateActivityFeedEventFilter = async (
const response = await axiosClient.put< const response = await axiosClient.put<
Filters[], Filters[],
AxiosResponse<{ AxiosResponse<ActivityFeedSettings>
config_type: string;
config_value: EventFilter[];
}>
>(url, payload, configOptions); >(url, payload, configOptions);
return response.data.config_value; return response.data.config_value;
}; };
export const updateFilters = async (data: ActivityFeedSettings) => {
const url = `${BASE_URL}`;
const response = await axiosClient.put<
ActivityFeedSettings,
AxiosResponse<ActivityFeedSettings>
>(url, data);
return response.data;
};
export const resetAllFilters = async () => {
const url = `${BASE_URL}/resetFilters`;
const response = await axiosClient.post<
null,
AxiosResponse<ActivityFeedSettings>
>(url);
return response.data;
};

View File

@ -1,7 +1,16 @@
.ant-collapse > .ant-collapse-item > .ant-collapse-header { .activity-feed-settings-tree {
align-items: center; .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 { .ant-tree-node-content-wrapper {
margin-right: 0.4rem; padding: 0;
}
margin-left: -8px;
} }

View File

@ -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 { AxiosError } from 'axios';
import { import { cloneDeep, isUndefined, map, startCase } from 'lodash';
cloneDeep,
isArray,
isEmpty,
isUndefined,
map,
startCase,
} from 'lodash';
import React, { Key, useEffect, useState } from 'react'; 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 { import {
createOrUpdateActivityFeedEventFilter, ActivityFeedSettings,
getActivityFeedEventFilters, getActivityFeedEventFilters,
resetAllFilters,
updateFilters,
} from '../../axiosAPIs/eventFiltersAPI'; } from '../../axiosAPIs/eventFiltersAPI';
import Loader from '../../components/Loader/Loader'; import Loader from '../../components/Loader/Loader';
import { TERM_ALL } from '../../constants/constants'; 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 jsonData from '../../jsons/en';
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
import {
ActivityFeedEntity,
formData,
} from './ActivityFeedSettingsPage.constants';
import './ActivityFeedSettingsPage.style.less'; import './ActivityFeedSettingsPage.style.less';
import { getPayloadFromSelected } from './ActivityFeedSettingsPage.utils'; import { getEventFilterFromTree } from './ActivityFeedSettingsPage.utils';
const ActivityFeedSettingsPage: React.FC = () => { const ActivityFeedSettingsPage: React.FC = () => {
const [eventFilters, setEventFilters] = useState<EventFilter[]>(); const [eventFilters, setEventFilters] = useState<EventFilter[]>();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedKeys, setSelectedKeys] = useState<string | string[]>([]);
const [selectedKey, setSelectedKey] = useState<string>(); const [selectedKey, setSelectedKey] = useState<string>();
const [checkedKeys, setCheckedKeys] = useState<string[]>([]); const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
const [updatedTree, setUpdatedTree] = useState<Record<string, string[]>>(); const [updatedTree, setUpdatedTree] = useState<Record<string, string[]>>();
@ -52,17 +45,11 @@ const ActivityFeedSettingsPage: React.FC = () => {
} }
}; };
const createActivityFeed = async ( const createActivityFeed = async (req: ActivityFeedSettings) => {
entityName: string,
selectedData: Filters[]
) => {
try { try {
setLoading(true); setLoading(true);
const data = await createOrUpdateActivityFeedEventFilter( const data = await updateFilters(req);
entityName, const filteredData = data.config_value?.filter(
selectedData
);
const filteredData = data?.filter(
({ entityType }) => entityType !== TERM_ALL ({ entityType }) => entityType !== TERM_ALL
); );
@ -77,23 +64,14 @@ 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[]) => { const generateTreeData = (entityType: string, data?: Filters[]) => {
return data?.map(({ eventType, include }) => { return [
{
key: entityType,
title: <strong>{startCase(entityType)}</strong>,
data: true,
children:
data?.map(({ eventType, include, exclude }) => {
const key = `${entityType}-${eventType}` as string; const key = `${entityType}-${eventType}` as string;
return { return {
@ -101,28 +79,25 @@ const ActivityFeedSettingsPage: React.FC = () => {
title: startCase(eventType), title: startCase(eventType),
data: include, data: include,
children: children:
eventType === 'entityUpdated' (include?.length === 1 && include[0] === TERM_ALL) ||
? [ (exclude?.length === 1 && exclude[0] === TERM_ALL)
{ ? undefined
key: `${key}-owner`, : [
title: 'Owner', ...(include?.map((inc) => ({
}, key: `${key}-${inc}`,
{ title: startCase(inc),
key: `${key}-description`, data: true,
title: 'Description', })) || []),
}, ...(exclude?.map((ex) => ({
{ key: `${key}-${ex}`,
key: `${key}-tags`, title: startCase(ex),
title: 'Tags', data: false,
}, })) || []),
{ ],
key: `${key}-followers`,
title: 'Followers',
},
]
: undefined,
}; };
}); }) || [],
},
];
}; };
const getCheckedKeys = (eventFilters: EventFilter[]) => { const getCheckedKeys = (eventFilters: EventFilter[]) => {
@ -164,15 +139,15 @@ const ActivityFeedSettingsPage: React.FC = () => {
const onSave = (event: React.MouseEvent<HTMLElement, MouseEvent>) => { const onSave = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
event.stopPropagation(); event.stopPropagation();
if (!isUndefined(updatedTree) && selectedKey) { if (!isUndefined(updatedTree) && selectedKey && eventFilters) {
const deepClonedTree = cloneDeep(updatedTree); const deepClonedTree = cloneDeep(updatedTree);
const selectedTree = { const data = {
[selectedKey]: deepClonedTree[selectedKey], config_type: SettingType.ActivityFeedFilterSetting,
}; config_value: getEventFilterFromTree(deepClonedTree, eventFilters),
const value = getPayloadFromSelected(selectedTree, selectedKey); } as ActivityFeedSettings;
createActivityFeed(selectedKey, value as Filters[]); createActivityFeed(data);
setUpdatedTree(undefined); setUpdatedTree(undefined);
setSelectedKey(undefined); setSelectedKey(undefined);
} }
@ -186,7 +161,26 @@ const ActivityFeedSettingsPage: React.FC = () => {
const checkKeys = getCheckedKeys(eventFilters as EventFilter[]); const checkKeys = getCheckedKeys(eventFilters as EventFilter[]);
setCheckedKeys(checkKeys); 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 ? ( return loading ? (
<Col span={24}> <Col span={24}>
@ -195,77 +189,48 @@ const ActivityFeedSettingsPage: React.FC = () => {
) : ( ) : (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={24}> <Col span={24}>
<Space align="baseline" className="tw-flex tw-justify-between">
<Typography.Title level={5} type="secondary"> <Typography.Title level={5} type="secondary">
Activity Feed Activity Feed
</Typography.Title> </Typography.Title>
<Typography.Link onClick={handleExpandAll}>
{selectedKeys.length === eventFilters?.length
? 'Collapse All'
: 'Expand All'}
</Typography.Link>
</Space>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Collapse <Card size="small">
destroyInactivePanel {eventFilters &&
activeKey={selectedKeys} map(eventFilters, ({ entityType, filters }, index) => (
className="activity-feed-collapse"
expandIcon={({ isActive }) => {
return (
<>
{isActive ? (
<Row className="arrow">
<DownArrow />{' '}
</Row>
) : (
<Row className="arrow">
<RightArrow />
</Row>
)}
</>
);
}}
onChange={handleExpandStateChange}>
{map(eventFilters, ({ entityType }) => (
<> <>
{entityType !== TERM_ALL ? ( {entityType !== TERM_ALL ? (
<Collapse.Panel <div className="tw-rounded-border" key={entityType}>
extra={
<Button
disabled={
!updatedTree ||
isUndefined(updatedTree[entityType]) ||
isEmpty(updatedTree[entityType])
}
type="primary"
onClick={(event) => onSave(event)}>
Save
</Button>
}
header={
<Row>
<Typography.Text strong>
{ActivityFeedEntity[entityType]}
</Typography.Text>
</Row>
}
key={entityType}>
<Tree <Tree
checkable checkable
defaultExpandAll
className="activity-feed-settings-tree"
defaultCheckedKeys={checkedKeys} defaultCheckedKeys={checkedKeys}
icon={null}
key={entityType} key={entityType}
treeData={generateTreeData(entityType, formData)} treeData={generateTreeData(entityType, filters)}
onCheck={(keys) => onCheck={(keys) =>
handleTreeCheckChange(keys as Key[], entityType) handleTreeCheckChange(keys as Key[], entityType)
} }
/> />
</Collapse.Panel> {index !== eventFilters?.length - 1 && <Divider />}
</div>
) : null} ) : null}
</> </>
))} ))}
</Collapse> </Card>
</Col> </Col>
<Col>
<Space direction="horizontal" size={16}>
<Button type="primary" onClick={onSave}>
Save
</Button>
<Button type="text" onClick={handleResetClick}>
Reset all
</Button>
</Space>
</Col>
<Col span={24} />
<Col span={24} />
</Row> </Row>
); );
}; };

View File

@ -1,17 +1,24 @@
import { isEmpty, isUndefined } from 'lodash'; import { intersection, isEmpty, isUndefined, xor } from 'lodash';
import { Filters } from '../../generated/settings/settings'; import {
EventFilter,
EventType,
Filters,
} from '../../generated/settings/settings';
import { getDiffArray } from '../../utils/CommonUtils'; import { getDiffArray } from '../../utils/CommonUtils';
const entityUpdatedFields = ['description', 'owner', 'tags', 'followers'];
export const getPayloadFromSelected = ( export const getPayloadFromSelected = (
selectedOptions: Record<string, string[]>, selectedOptions: Record<string, string[]>,
selectedKey?: string selectedKey: string,
selectedEntityEventUpdatedFields: string[]
): void | Array<Filters> => { ): void | Array<Filters> => {
const nonUpdatedFields = [] as string[]; const nonUpdatedFields = [] as string[];
const resultArr = []; const resultArr = [];
if (isUndefined(selectedOptions) || isEmpty(selectedKey)) { if (
isUndefined(selectedOptions) &&
isEmpty(selectedKey) &&
selectedEntityEventUpdatedFields
) {
return [] as Filters[]; return [] as Filters[];
} }
@ -23,7 +30,7 @@ export const getPayloadFromSelected = (
value.reduce((valueAcc: any, name: string) => { value.reduce((valueAcc: any, name: string) => {
const selected = name.split('-'); const selected = name.split('-');
if (selected[1] !== 'entityUpdated') { if (selected[1] !== EventType.EntityUpdated) {
return [ return [
...valueAcc, ...valueAcc,
{ {
@ -48,12 +55,56 @@ export const getPayloadFromSelected = (
); );
resultArr.push({ resultArr.push({
eventType: 'entityUpdated', eventType: EventType.EntityUpdated,
include: selectedUpdatedData, include: selectedUpdatedData,
exclude: getDiffArray(entityUpdatedFields, selectedUpdatedData), exclude: getDiffArray(
selectedEntityEventUpdatedFields,
selectedUpdatedData
),
}); });
} }
return resultArr as Filters[]; return resultArr as Filters[];
} }
}; };
export const getEventFilterFromTree = (
updatedTree: Record<string, string[]>,
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,
};
}),
}));
};