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.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<EventFilter> 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<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
@Path("/{settingName}")
@Operation(

View File

@ -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<ActivityFeedSettings>(
`${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<ActivityFeedSettings>
>(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<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 {
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;
}

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 {
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<EventFilter[]>();
const [loading, setLoading] = useState(true);
const [selectedKeys, setSelectedKeys] = useState<string | string[]>([]);
const [selectedKey, setSelectedKey] = useState<string>();
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
const [updatedTree, setUpdatedTree] = useState<Record<string, string[]>>();
@ -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: <strong>{startCase(entityType)}</strong>,
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<HTMLElement, 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 ? (
<Col span={24}>
@ -195,77 +189,48 @@ const ActivityFeedSettingsPage: React.FC = () => {
) : (
<Row gutter={[16, 16]}>
<Col span={24}>
<Space align="baseline" className="tw-flex tw-justify-between">
<Typography.Title level={5} type="secondary">
Activity Feed
</Typography.Title>
<Typography.Link onClick={handleExpandAll}>
{selectedKeys.length === eventFilters?.length
? 'Collapse All'
: 'Expand All'}
</Typography.Link>
</Space>
<Typography.Title level={5} type="secondary">
Activity Feed
</Typography.Title>
</Col>
<Col span={24}>
<Collapse
destroyInactivePanel
activeKey={selectedKeys}
className="activity-feed-collapse"
expandIcon={({ isActive }) => {
return (
<Card size="small">
{eventFilters &&
map(eventFilters, ({ entityType, filters }, index) => (
<>
{isActive ? (
<Row className="arrow">
<DownArrow />{' '}
</Row>
) : (
<Row className="arrow">
<RightArrow />
</Row>
)}
</>
);
}}
onChange={handleExpandStateChange}>
{map(eventFilters, ({ entityType }) => (
<>
{entityType !== TERM_ALL ? (
<Collapse.Panel
extra={
<Button
disabled={
!updatedTree ||
isUndefined(updatedTree[entityType]) ||
isEmpty(updatedTree[entityType])
{entityType !== TERM_ALL ? (
<div className="tw-rounded-border" key={entityType}>
<Tree
checkable
defaultExpandAll
className="activity-feed-settings-tree"
defaultCheckedKeys={checkedKeys}
icon={null}
key={entityType}
treeData={generateTreeData(entityType, filters)}
onCheck={(keys) =>
handleTreeCheckChange(keys as Key[], entityType)
}
type="primary"
onClick={(event) => onSave(event)}>
Save
</Button>
}
header={
<Row>
<Typography.Text strong>
{ActivityFeedEntity[entityType]}
</Typography.Text>
</Row>
}
key={entityType}>
<Tree
checkable
defaultCheckedKeys={checkedKeys}
key={entityType}
treeData={generateTreeData(entityType, formData)}
onCheck={(keys) =>
handleTreeCheckChange(keys as Key[], entityType)
}
/>
</Collapse.Panel>
) : null}
</>
))}
</Collapse>
/>
{index !== eventFilters?.length - 1 && <Divider />}
</div>
) : null}
</>
))}
</Card>
</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>
);
};

View File

@ -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<string, string[]>,
selectedKey?: string
selectedKey: string,
selectedEntityEventUpdatedFields: string[]
): void | Array<Filters> => {
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<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,
};
}),
}));
};