fix(ui): assets virtual scroll (#10820)

* fix(ui): assets virtual scroll
fetch tags before make patch api call

* fix append issue
This commit is contained in:
Chirag Madlani 2023-03-29 18:27:51 +05:30 committed by GitHub
parent c6538a38bf
commit 4241457cfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 199 additions and 81 deletions

View File

@ -18,7 +18,7 @@ import Loader from 'components/Loader/Loader';
import { PAGE_SIZE_MEDIUM } from 'constants/constants';
import { EntityType } from 'enums/entity.enum';
import { SearchIndex } from 'enums/search.enum';
import { compare, Operation } from 'fast-json-patch';
import { compare } from 'fast-json-patch';
import { cloneDeep, groupBy, map, startCase } from 'lodash';
import { EntityDetailUnion } from 'Models';
import VirtualList from 'rc-virtual-list';
@ -30,37 +30,48 @@ import {
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { patchDashboardDetails } from 'rest/dashboardAPI';
import { patchMlModelDetails } from 'rest/mlModelAPI';
import { patchContainerDetails } from 'rest/objectStoreAPI';
import { patchPipelineDetails } from 'rest/pipelineAPI';
import { searchQuery } from 'rest/searchAPI';
import { patchTableDetails } from 'rest/tableAPI';
import { patchTopicDetails } from 'rest/topicsAPI';
import {
getAPIfromSource,
getEntityAPIfromSource,
} from 'utils/Assets/AssetsUtils';
import { getCountBadge } from 'utils/CommonUtils';
import { getQueryFilterToExcludeTerm } from 'utils/GlossaryUtils';
import {
AssetFilterKeys,
AssetSelectionModalProps,
AssetsUnion,
MapPatchAPIResponse,
} from './AssetSelectionModal.interface';
export const AssetSelectionModal = ({
glossaryFQN,
onCancel,
onSave,
open,
}: AssetSelectionModalProps) => {
const { t } = useTranslation();
const [search, setSearch] = useState('');
const [items, setItems] = useState<EntityDetailUnion[]>([]);
const [itemCount, setItemCount] = useState<Record<AssetFilterKeys, number>>();
const [itemCount, setItemCount] = useState<Record<AssetFilterKeys, number>>({
all: 0,
table: 0,
pipeline: 0,
mlmodel: 0,
container: 0,
topic: 0,
dashboard: 0,
});
const [selectedItems, setSelectedItems] =
useState<Map<string, EntityDetailUnion>>();
const [isLoading, setIsLoading] = useState(false);
const [activeFilter, setActiveFilter] = useState<AssetFilterKeys>('all');
const [pageNumber, setPageNumber] = useState(1);
useEffect(() => {
if (open) {
fetchEntities();
}
}, [open]);
const fetchEntities = async (searchText = '', page = 1) => {
try {
setIsLoading(true);
@ -80,18 +91,41 @@ export const AssetSelectionModal = ({
});
const groupedArray = groupBy(res.hits.hits, '_source.entityType');
const isAppend = page !== 1;
const tableCount = groupedArray[EntityType.TABLE]?.length ?? 0;
const containerCount = groupedArray[EntityType.CONTAINER]?.length ?? 0;
const pipelineCount = groupedArray[EntityType.PIPELINE]?.length ?? 0;
const dashboardCount = groupedArray[EntityType.DASHBOARD]?.length ?? 0;
const topicCount = groupedArray[EntityType.TOPIC]?.length ?? 0;
const mlmodelCount = groupedArray[EntityType.MLMODEL]?.length ?? 0;
setItemCount({
setItemCount((prevCount) => ({
...prevCount,
all: res.hits.total.value,
[EntityType.TABLE]: groupedArray[EntityType.TABLE]?.length ?? 0,
[EntityType.PIPELINE]: groupedArray[EntityType.PIPELINE]?.length ?? 0,
[EntityType.MLMODEL]: groupedArray[EntityType.MLMODEL]?.length ?? 0,
[EntityType.TOPIC]: groupedArray[EntityType.TOPIC]?.length ?? 0,
[EntityType.DASHBOARD]: groupedArray[EntityType.DASHBOARD]?.length ?? 0,
[EntityType.CONTAINER]: groupedArray[EntityType.CONTAINER]?.length ?? 0,
});
...(isAppend
? {
table: prevCount.table + tableCount,
pipeline: prevCount.pipeline + pipelineCount,
mlmodel: prevCount.mlmodel + mlmodelCount,
container: prevCount.container + containerCount,
topic: prevCount.topic + topicCount,
dashboard: prevCount.dashboard + dashboardCount,
}
: {
table: tableCount,
pipeline: pipelineCount,
mlmodel: mlmodelCount,
container: containerCount,
topic: topicCount,
dashboard: dashboardCount,
}),
}));
setActiveFilter('all');
setItems(res.hits.hits);
setItems(
page === 1
? res.hits.hits
: (prevItems) => [...prevItems, ...res.hits.hits]
);
setPageNumber(page);
} catch (error) {
console.error(error);
@ -131,48 +165,51 @@ export const AssetSelectionModal = ({
}
};
const getAPIfromSource = (
source: AssetsUnion
): ((
id: string,
jsonPatch: Operation[]
) => Promise<MapPatchAPIResponse[typeof source]>) => {
switch (source) {
case EntityType.TABLE:
return patchTableDetails;
case EntityType.DASHBOARD:
return patchDashboardDetails;
case EntityType.MLMODEL:
return patchMlModelDetails;
case EntityType.PIPELINE:
return patchPipelineDetails;
case EntityType.TOPIC:
return patchTopicDetails;
case EntityType.CONTAINER:
return patchContainerDetails;
}
};
const handleSave = async () => {
setIsLoading(true);
const promises = [...(selectedItems?.values() ?? [])].map((item) => {
const jsonPatch = compare(
{ tags: item.tags },
{
tags: [
...item.tags,
{ tagFQN: glossaryFQN, source: 'Glossary', labelType: 'Manual' },
],
}
);
const api = getAPIfromSource(item.entityType);
return api(item.id, jsonPatch);
});
const entityDetails = [...(selectedItems?.values() ?? [])].map((item) =>
getEntityAPIfromSource(item.entityType)(item.fullyQualifiedName, 'tags')
);
try {
await Promise.all(promises);
const entityDetailsResponse = await Promise.allSettled(entityDetails);
const map = new Map();
entityDetailsResponse.forEach((response) => {
if (response.status === 'fulfilled') {
const entity = response.value;
entity && map.set(entity.fullyQualifiedName, entity.tags);
}
});
const patchAPIPromises = [...(selectedItems?.values() ?? [])]
.map((item) => {
if (map.has(item.fullyQualifiedName)) {
const jsonPatch = compare(
{ tags: map.get(item.fullyQualifiedName) },
{
tags: [
...(item.tags ?? []),
{
tagFQN: glossaryFQN,
source: 'Glossary',
labelType: 'Manual',
},
],
}
);
const api = getAPIfromSource(item.entityType);
return api(item.id, jsonPatch);
}
return;
})
.filter(Boolean);
await Promise.all(patchAPIPromises);
onSave && onSave();
onCancel();
} catch (error) {
console.error(error);
@ -181,13 +218,9 @@ export const AssetSelectionModal = ({
}
};
useEffect(() => {
fetchEntities();
}, []);
const onScroll: UIEventHandler<HTMLElement> = (e) => {
if (e.currentTarget.scrollHeight - e.currentTarget.scrollTop === 500) {
fetchEntities(search, pageNumber + 1);
!isLoading && fetchEntities(search, pageNumber + 1);
}
};

View File

@ -40,6 +40,7 @@ export interface GlossaryHeaderProps {
isGlossary: boolean;
onUpdate: (data: GlossaryTerm | Glossary) => void;
onDelete: (id: string) => void;
onAssetsUpdate?: () => void;
}
const GlossaryHeader = ({
@ -48,6 +49,7 @@ const GlossaryHeader = ({
onUpdate,
onDelete,
isGlossary,
onAssetsUpdate,
}: GlossaryHeaderProps) => {
const { t } = useTranslation();
@ -219,6 +221,7 @@ const GlossaryHeader = ({
isGlossary={isGlossary}
permission={permissions}
selectedData={selectedData}
onAssetsUpdate={onAssetsUpdate}
onEntityDelete={onDelete}
/>
</div>

View File

@ -27,7 +27,7 @@ import { GlossaryTerm } from 'generated/entity/data/glossaryTerm';
import { EntityHistory } from 'generated/type/entityHistory';
import { toString } from 'lodash';
import { LoadingState } from 'Models';
import React, { useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import {
@ -51,6 +51,7 @@ interface GlossaryHeaderButtonsProps {
selectedData: Glossary | GlossaryTerm;
permission: OperationPermission;
onEntityDelete: (id: string) => void;
onAssetsUpdate?: () => void;
}
const GlossaryHeaderButtons = ({
@ -59,6 +60,7 @@ const GlossaryHeaderButtons = ({
selectedData,
permission,
onEntityDelete,
onAssetsUpdate,
}: GlossaryHeaderButtonsProps) => {
const { t } = useTranslation();
const { action, glossaryName: glossaryFqn } =
@ -74,12 +76,12 @@ const GlossaryHeaderButtons = ({
[action]
);
const handleAddGlossaryTermClick = (glossaryFQN: string) => {
if (glossaryFQN) {
const activeTerm = glossaryFQN.split(FQN_SEPARATOR_CHAR);
const handleAddGlossaryTermClick = useCallback(() => {
if (glossaryFqn) {
const activeTerm = glossaryFqn.split(FQN_SEPARATOR_CHAR);
const glossary = activeTerm[0];
if (activeTerm.length > 1) {
history.push(getAddGlossaryTermsPath(glossary, glossaryFQN));
history.push(getAddGlossaryTermsPath(glossary, glossaryFqn));
} else {
history.push(getAddGlossaryTermsPath(glossary));
}
@ -88,7 +90,7 @@ const GlossaryHeaderButtons = ({
getAddGlossaryTermsPath(selectedData.fullyQualifiedName ?? '')
);
}
};
}, [glossaryFqn]);
const handleGlossaryExport = () =>
history.push(
@ -129,7 +131,7 @@ const GlossaryHeaderButtons = ({
{
label: t('label.glossary-term'),
key: '1',
onClick: () => handleAddGlossaryTermClick(glossaryFqn),
onClick: handleAddGlossaryTermClick,
},
{
label: t('label.asset-plural'),
@ -248,7 +250,7 @@ const GlossaryHeaderButtons = ({
data-testid="add-new-tag-button-header"
size="middle"
type="primary"
onClick={() => handleAddGlossaryTermClick(glossaryFqn)}>
onClick={handleAddGlossaryTermClick}>
{t('label.add-entity', { entity: t('label.term-lowercase') })}
</Button>
) : (
@ -333,6 +335,7 @@ const GlossaryHeaderButtons = ({
glossaryFQN={selectedData.fullyQualifiedName}
open={showAddAssets}
onCancel={() => setShowAddAssets(false)}
onSave={onAssetsUpdate}
/>
)}
</>

View File

@ -124,6 +124,10 @@ const GlossaryTermsV1 = ({
isGlossary={false}
permissions={permissions}
selectedData={glossaryTerm}
onAssetsUpdate={() =>
glossaryTerm.fullyQualifiedName &&
fetchGlossaryTermAssets(glossaryTerm.fullyQualifiedName)
}
onDelete={handleGlossaryTermDelete}
onUpdate={(data) => handleGlossaryTermUpdate(data as GlossaryTerm)}
/>

View File

@ -37,7 +37,7 @@ import {
type SuggestionProp = {
searchText: string;
searchCriteria: SearchIndex | null;
searchCriteria?: SearchIndex;
isOpen: boolean;
setIsOpen: (value: boolean) => void;
};
@ -138,7 +138,7 @@ const Suggestions = ({
useEffect(() => {
if (!isMounting.current && searchText) {
getSuggestions(searchText, searchCriteria ?? undefined)
getSuggestions(searchText, searchCriteria)
.then((res) => {
if (res.data) {
setOptions(

View File

@ -39,7 +39,6 @@ import { refreshPage } from 'utils/CommonUtils';
import { isCommandKeyPress, Keys } from 'utils/KeyboardUtil';
import AppState from '../../AppState';
import Logo from '../../assets/svg/logo-monogram.svg';
import {
NOTIFICATION_READ_TIMER,
ROUTES,
@ -113,12 +112,10 @@ const NavBar = ({
useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>('Task');
const [isImgUrlValid, setIsImgUrlValid] = useState<boolean>(true);
const [searchCriteria, setSearchCriteria] = useState<SearchIndex | null>(
null
);
const [searchCriteria, setSearchCriteria] = useState<SearchIndex | ''>('');
const globalSearchOptions = useMemo(
() => [
{ value: null, label: t('label.all') },
{ value: '', label: t('label.all') },
{ value: SearchIndex.TABLE, label: t('label.table') },
{ value: SearchIndex.TOPIC, label: t('label.topic') },
{ value: SearchIndex.DASHBOARD, label: t('label.dashboard') },
@ -131,7 +128,7 @@ const NavBar = ({
[]
);
const updateSearchCriteria = (criteria: SearchIndex | null) => {
const updateSearchCriteria = (criteria: SearchIndex | '') => {
setSearchCriteria(criteria);
handleSearchChange(searchValue);
};
@ -507,7 +504,9 @@ const NavBar = ({
) : (
<Suggestions
isOpen={isSearchBoxOpen}
searchCriteria={searchCriteria}
searchCriteria={
searchCriteria === '' ? undefined : searchCriteria
}
searchText={suggestionSearch}
setIsOpen={handleSearchBoxOpen}
/>

View File

@ -41,7 +41,7 @@ import {
patchGlossaryTerm,
} from 'rest/glossaryAPI';
import { checkPermission } from 'utils/PermissionsUtils';
import { getGlossaryPath } from 'utils/RouterUtils';
import { getGlossaryPath, getGlossaryTermsPath } from 'utils/RouterUtils';
import { showErrorToast, showSuccessToast } from 'utils/ToastUtils';
import GlossaryLeftPanel from '../GlossaryLeftPanel/GlossaryLeftPanel.component';
import GlossaryRightPanel from '../GlossaryRightPanel/GlossaryRightPanel.component';
@ -124,6 +124,11 @@ const GlossaryPage = () => {
glossaries.find((glossary) => glossary.name === glossaryFqn) ||
glossaries[0]
);
!glossaryFqn &&
glossaries[0].fullyQualifiedName &&
history.replace(
getGlossaryTermsPath(glossaries[0].fullyQualifiedName)
);
setIsRightPanelLoading(false);
}
}

View File

@ -48,7 +48,10 @@ export const getContainers = async (args: {
return response.data;
};
export const getContainerByName = async (name: string, fields: string) => {
export const getContainerByName = async (
name: string,
fields: string | string[]
) => {
const response = await APIClient.get<Container>(
`containers/name/${name}?fields=${fields}`
);

View File

@ -0,0 +1,68 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
AssetsUnion,
MapPatchAPIResponse,
} from 'components/Assets/AssetsSelectionModal/AssetSelectionModal.interface';
import { EntityType } from 'enums/entity.enum';
import { Operation } from 'fast-json-patch';
import { getDashboardByFqn, patchDashboardDetails } from 'rest/dashboardAPI';
import { getMlModelByFQN, patchMlModelDetails } from 'rest/mlModelAPI';
import { getContainerByName, patchContainerDetails } from 'rest/objectStoreAPI';
import { getPipelineByFqn, patchPipelineDetails } from 'rest/pipelineAPI';
import { getTableDetailsByFQN, patchTableDetails } from 'rest/tableAPI';
import { getTopicByFqn, patchTopicDetails } from 'rest/topicsAPI';
export const getAPIfromSource = (
source: AssetsUnion
): ((
id: string,
jsonPatch: Operation[]
) => Promise<MapPatchAPIResponse[typeof source]>) => {
switch (source) {
case EntityType.TABLE:
return patchTableDetails;
case EntityType.DASHBOARD:
return patchDashboardDetails;
case EntityType.MLMODEL:
return patchMlModelDetails;
case EntityType.PIPELINE:
return patchPipelineDetails;
case EntityType.TOPIC:
return patchTopicDetails;
case EntityType.CONTAINER:
return patchContainerDetails;
}
};
export const getEntityAPIfromSource = (
source: AssetsUnion
): ((
id: string,
queryFields: string | string[]
) => Promise<MapPatchAPIResponse[typeof source]>) => {
switch (source) {
case EntityType.TABLE:
return getTableDetailsByFQN;
case EntityType.DASHBOARD:
return getDashboardByFqn;
case EntityType.MLMODEL:
return getMlModelByFQN;
case EntityType.PIPELINE:
return getPipelineByFqn;
case EntityType.TOPIC:
return getTopicByFqn;
case EntityType.CONTAINER:
return getContainerByName;
}
};