diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Assets/AssetsSelectionModal/AssetSelectionModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Assets/AssetsSelectionModal/AssetSelectionModal.tsx index afb5465f460..50690b7b7ff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Assets/AssetsSelectionModal/AssetSelectionModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Assets/AssetsSelectionModal/AssetSelectionModal.tsx @@ -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([]); - const [itemCount, setItemCount] = useState>(); + const [itemCount, setItemCount] = useState>({ + all: 0, + table: 0, + pipeline: 0, + mlmodel: 0, + container: 0, + topic: 0, + dashboard: 0, + }); const [selectedItems, setSelectedItems] = useState>(); const [isLoading, setIsLoading] = useState(false); const [activeFilter, setActiveFilter] = useState('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) => { - 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 = (e) => { if (e.currentTarget.scrollHeight - e.currentTarget.scrollTop === 500) { - fetchEntities(search, pageNumber + 1); + !isLoading && fetchEntities(search, pageNumber + 1); } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx index c8b79fce0e2..dd7a36bced4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx @@ -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} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeaderButtons/GlossaryHeaderButtons.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeaderButtons/GlossaryHeaderButtons.component.tsx index 19c0d209bb7..58557713d2b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeaderButtons/GlossaryHeaderButtons.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeaderButtons/GlossaryHeaderButtons.component.tsx @@ -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') })} ) : ( @@ -333,6 +335,7 @@ const GlossaryHeaderButtons = ({ glossaryFQN={selectedData.fullyQualifiedName} open={showAddAssets} onCancel={() => setShowAddAssets(false)} + onSave={onAssetsUpdate} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/GlossaryTerms/GlossaryTermsV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/GlossaryTerms/GlossaryTermsV1.component.tsx index 95127991ec4..85a2b2bbd86 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/GlossaryTerms/GlossaryTermsV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/GlossaryTerms/GlossaryTermsV1.component.tsx @@ -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)} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/app-bar/Suggestions.tsx b/openmetadata-ui/src/main/resources/ui/src/components/app-bar/Suggestions.tsx index 4417f6d1391..895e87c5844 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/app-bar/Suggestions.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/app-bar/Suggestions.tsx @@ -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( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/nav-bar/NavBar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/nav-bar/NavBar.tsx index 45e8886a53f..1f73260b5da 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/nav-bar/NavBar.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/nav-bar/NavBar.tsx @@ -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(false); const [activeTab, setActiveTab] = useState('Task'); const [isImgUrlValid, setIsImgUrlValid] = useState(true); - const [searchCriteria, setSearchCriteria] = useState( - null - ); + const [searchCriteria, setSearchCriteria] = useState(''); 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 = ({ ) : ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.component.tsx index 201dac76b2f..f5a7d6a58a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/Glossary/GlossaryPage/GlossaryPage.component.tsx @@ -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); } } diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/objectStoreAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/objectStoreAPI.ts index 56b544cd30e..2bc0401df06 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/objectStoreAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/objectStoreAPI.ts @@ -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( `containers/name/${name}?fields=${fields}` ); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Assets/AssetsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Assets/AssetsUtils.ts new file mode 100644 index 00000000000..4b225a26be2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Assets/AssetsUtils.ts @@ -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) => { + 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) => { + 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; + } +};