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 { PAGE_SIZE_MEDIUM } from 'constants/constants';
import { EntityType } from 'enums/entity.enum'; import { EntityType } from 'enums/entity.enum';
import { SearchIndex } from 'enums/search.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 { cloneDeep, groupBy, map, startCase } from 'lodash';
import { EntityDetailUnion } from 'Models'; import { EntityDetailUnion } from 'Models';
import VirtualList from 'rc-virtual-list'; import VirtualList from 'rc-virtual-list';
@ -30,37 +30,48 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { useTranslation } from 'react-i18next'; 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 { searchQuery } from 'rest/searchAPI';
import { patchTableDetails } from 'rest/tableAPI'; import {
import { patchTopicDetails } from 'rest/topicsAPI'; getAPIfromSource,
getEntityAPIfromSource,
} from 'utils/Assets/AssetsUtils';
import { getCountBadge } from 'utils/CommonUtils'; import { getCountBadge } from 'utils/CommonUtils';
import { getQueryFilterToExcludeTerm } from 'utils/GlossaryUtils'; import { getQueryFilterToExcludeTerm } from 'utils/GlossaryUtils';
import { import {
AssetFilterKeys, AssetFilterKeys,
AssetSelectionModalProps, AssetSelectionModalProps,
AssetsUnion,
MapPatchAPIResponse,
} from './AssetSelectionModal.interface'; } from './AssetSelectionModal.interface';
export const AssetSelectionModal = ({ export const AssetSelectionModal = ({
glossaryFQN, glossaryFQN,
onCancel, onCancel,
onSave,
open, open,
}: AssetSelectionModalProps) => { }: AssetSelectionModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [items, setItems] = useState<EntityDetailUnion[]>([]); 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] = const [selectedItems, setSelectedItems] =
useState<Map<string, EntityDetailUnion>>(); useState<Map<string, EntityDetailUnion>>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [activeFilter, setActiveFilter] = useState<AssetFilterKeys>('all'); const [activeFilter, setActiveFilter] = useState<AssetFilterKeys>('all');
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1);
useEffect(() => {
if (open) {
fetchEntities();
}
}, [open]);
const fetchEntities = async (searchText = '', page = 1) => { const fetchEntities = async (searchText = '', page = 1) => {
try { try {
setIsLoading(true); setIsLoading(true);
@ -80,18 +91,41 @@ export const AssetSelectionModal = ({
}); });
const groupedArray = groupBy(res.hits.hits, '_source.entityType'); 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, all: res.hits.total.value,
[EntityType.TABLE]: groupedArray[EntityType.TABLE]?.length ?? 0, ...(isAppend
[EntityType.PIPELINE]: groupedArray[EntityType.PIPELINE]?.length ?? 0, ? {
[EntityType.MLMODEL]: groupedArray[EntityType.MLMODEL]?.length ?? 0, table: prevCount.table + tableCount,
[EntityType.TOPIC]: groupedArray[EntityType.TOPIC]?.length ?? 0, pipeline: prevCount.pipeline + pipelineCount,
[EntityType.DASHBOARD]: groupedArray[EntityType.DASHBOARD]?.length ?? 0, mlmodel: prevCount.mlmodel + mlmodelCount,
[EntityType.CONTAINER]: groupedArray[EntityType.CONTAINER]?.length ?? 0, 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'); setActiveFilter('all');
setItems(res.hits.hits); setItems(
page === 1
? res.hits.hits
: (prevItems) => [...prevItems, ...res.hits.hits]
);
setPageNumber(page); setPageNumber(page);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -131,37 +165,36 @@ 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 () => { const handleSave = async () => {
setIsLoading(true); setIsLoading(true);
const promises = [...(selectedItems?.values() ?? [])].map((item) => { const entityDetails = [...(selectedItems?.values() ?? [])].map((item) =>
getEntityAPIfromSource(item.entityType)(item.fullyQualifiedName, 'tags')
);
try {
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( const jsonPatch = compare(
{ tags: item.tags }, { tags: map.get(item.fullyQualifiedName) },
{ {
tags: [ tags: [
...item.tags, ...(item.tags ?? []),
{ tagFQN: glossaryFQN, source: 'Glossary', labelType: 'Manual' }, {
tagFQN: glossaryFQN,
source: 'Glossary',
labelType: 'Manual',
},
], ],
} }
); );
@ -169,10 +202,14 @@ export const AssetSelectionModal = ({
const api = getAPIfromSource(item.entityType); const api = getAPIfromSource(item.entityType);
return api(item.id, jsonPatch); return api(item.id, jsonPatch);
}); }
try { return;
await Promise.all(promises); })
.filter(Boolean);
await Promise.all(patchAPIPromises);
onSave && onSave();
onCancel(); onCancel();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -181,13 +218,9 @@ export const AssetSelectionModal = ({
} }
}; };
useEffect(() => {
fetchEntities();
}, []);
const onScroll: UIEventHandler<HTMLElement> = (e) => { const onScroll: UIEventHandler<HTMLElement> = (e) => {
if (e.currentTarget.scrollHeight - e.currentTarget.scrollTop === 500) { 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; isGlossary: boolean;
onUpdate: (data: GlossaryTerm | Glossary) => void; onUpdate: (data: GlossaryTerm | Glossary) => void;
onDelete: (id: string) => void; onDelete: (id: string) => void;
onAssetsUpdate?: () => void;
} }
const GlossaryHeader = ({ const GlossaryHeader = ({
@ -48,6 +49,7 @@ const GlossaryHeader = ({
onUpdate, onUpdate,
onDelete, onDelete,
isGlossary, isGlossary,
onAssetsUpdate,
}: GlossaryHeaderProps) => { }: GlossaryHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -219,6 +221,7 @@ const GlossaryHeader = ({
isGlossary={isGlossary} isGlossary={isGlossary}
permission={permissions} permission={permissions}
selectedData={selectedData} selectedData={selectedData}
onAssetsUpdate={onAssetsUpdate}
onEntityDelete={onDelete} onEntityDelete={onDelete}
/> />
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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