mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-10 14:57:57 +00:00
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:
parent
106ba985b3
commit
52b100fbc6
@ -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(
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user