mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-31 12:39:01 +00:00
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:
parent
c6538a38bf
commit
4241457cfb
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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)}
|
||||
/>
|
||||
|
@ -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(
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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}`
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user