diff --git a/openmetadata-ui/src/main/resources/ui/.eslintignore b/openmetadata-ui/src/main/resources/ui/.eslintignore index 96e99d6d882..6da13179a1b 100644 --- a/openmetadata-ui/src/main/resources/ui/.eslintignore +++ b/openmetadata-ui/src/main/resources/ui/.eslintignore @@ -19,6 +19,9 @@ node_modules/ build/ dist/ +# mockups +mock-api/ + # macOS .DS_Store diff --git a/openmetadata-ui/src/main/resources/ui/.prettierignore b/openmetadata-ui/src/main/resources/ui/.prettierignore index 2daf926143e..a6ccd474516 100644 --- a/openmetadata-ui/src/main/resources/ui/.prettierignore +++ b/openmetadata-ui/src/main/resources/ui/.prettierignore @@ -19,6 +19,9 @@ node_modules/ build/ dist/ +# mockups +mock-api/ + # Ignore files (Prettier has trouble parsing files without extension) .gitignore .prettierignore diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AddGlossaryTermPage/AddGlossaryTermPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AddGlossaryTermPage/AddGlossaryTermPage.component.tsx index 1491d4962c4..7cf1299b21a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AddGlossaryTermPage/AddGlossaryTermPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AddGlossaryTermPage/AddGlossaryTermPage.component.tsx @@ -21,12 +21,13 @@ import { getGlossariesByName, getGlossaryTermByFQN, } from '../../axiosAPIs/glossaryAPI'; -import { ROUTES } from '../../constants/constants'; +import { getGlossaryPath } from '../../constants/constants'; import { CreateGlossaryTerm } from '../../generated/api/data/createGlossaryTerm'; import { Glossary } from '../../generated/entity/data/glossary'; import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm'; import { useAuth } from '../../hooks/authHooks'; import useToastContext from '../../hooks/useToastContext'; +import jsonData from '../../jsons/en'; import AddGlossaryTerm from '../AddGlossaryTerm/AddGlossaryTerm.component'; import PageContainerV1 from '../containers/PageContainerV1'; import Loader from '../Loader/Loader'; @@ -44,43 +45,65 @@ const AddGlossaryTermPage = () => { const [parentGlossaryData, setParentGlossaryData] = useState(); - const goToGlossary = () => { - history.push(ROUTES.GLOSSARY); + const goToGlossary = (name = '') => { + history.push(getGlossaryPath(name)); }; const handleCancel = () => { goToGlossary(); }; + const handleShowErrorToast = (errMessage: string) => { + showToast({ + variant: 'error', + body: errMessage, + }); + }; + + const handleSaveFailure = (errorMessage = '') => { + handleShowErrorToast( + errorMessage || jsonData['api-error-messages']['add-glossary-term-error'] + ); + setStatus('initial'); + }; + const onSave = (data: CreateGlossaryTerm) => { setStatus('waiting'); addGlossaryTerm(data) - .then(() => { - setStatus('success'); - setTimeout(() => { - setStatus('initial'); - goToGlossary(); - }, 500); + .then((res) => { + if (res.data) { + setStatus('success'); + setTimeout(() => { + setStatus('initial'); + goToGlossary(res?.data?.fullyQualifiedName); + }, 500); + } else { + handleSaveFailure(); + } }) .catch((err: AxiosError) => { - showToast({ - variant: 'error', - body: err.message || 'Something went wrong!', - }); - setStatus('initial'); + handleSaveFailure(err.response?.data?.message); }); }; const fetchGlossaryData = () => { getGlossariesByName(glossaryName, ['tags', 'owner', 'reviewers']) .then((res: AxiosResponse) => { - setGlossaryData(res.data); + if (res.data) { + setGlossaryData(res.data); + } else { + setGlossaryData(undefined); + handleShowErrorToast( + jsonData['api-error-messages']['fetch-glossary-error'] + ); + } }) .catch((err: AxiosError) => { - showToast({ - variant: 'error', - body: err.message || 'Error while fetching glossary!', - }); + setGlossaryData(undefined); + handleShowErrorToast( + err.response?.data?.message || + jsonData['api-error-messages']['fetch-glossary-error'] + ); }) .finally(() => setIsLoading(false)); }; @@ -93,14 +116,21 @@ const AddGlossaryTermPage = () => { 'tags', ]) .then((res: AxiosResponse) => { - setParentGlossaryData(res.data); + if (res.data) { + setParentGlossaryData(res.data); + } else { + setParentGlossaryData(undefined); + handleShowErrorToast( + jsonData['api-error-messages']['fetch-glossary-term-error'] + ); + } }) .catch((err: AxiosError) => { setParentGlossaryData(undefined); - showToast({ - variant: 'error', - body: err.message || 'Error while fetching glossary terms!', - }); + handleShowErrorToast( + err.response?.data?.message || + jsonData['api-error-messages']['fetch-glossary-term-error'] + ); }); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx index 33606f558ac..5808a444b74 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx @@ -58,25 +58,13 @@ type Props = { handleAddGlossaryTermClick: () => void; updateGlossary: (value: Glossary) => void; handleGlossaryTermUpdate: (value: GlossaryTerm) => void; - handleSelectedData: ( - data: Glossary | GlossaryTerm, - pos: string, - key: string - ) => void; + handleSelectedData: (key: string) => void; handleChildLoading: (status: boolean) => void; handleSearchText: (text: string) => void; onGlossaryDelete: (id: string) => void; onGlossaryTermDelete: (id: string) => void; onAssetPaginate: (num: number) => void; isChildLoading: boolean; - // handlePathChange: ( - // glossary: string, - // glossaryTermsFQN?: string | undefined - // ) => void; -}; - -type ModifiedDataNode = DataNode & { - data: Glossary | GlossaryTerm; }; const GlossaryV1 = ({ @@ -163,14 +151,8 @@ Props) => { const key = node.key as string; if (selectedKey !== key) { handleChildLoading(true); - const breadCrumbData = (treeRef.current?.state.keyEntities[key].nodes || - []) as ModifiedDataNode[]; - const selData = breadCrumbData[breadCrumbData.length - 1].data; - const pos = treeRef.current?.state.keyEntities[key].pos; - handleSelectedData(selData, pos as string, key); + handleSelectedData(key); } - // handlePathChange(key.split('.')[0], key); - // handleSelectedKey(key); }; useEffect(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts index 007d4e6834c..e94c96ae0ff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts @@ -295,8 +295,14 @@ export const getUserPath = (username: string) => { return path; }; -export const getGlossaryPath = () => { - return ROUTES.GLOSSARY; +export const getGlossaryPath = (fqn?: string) => { + let path = ROUTES.GLOSSARY; + if (fqn) { + path = ROUTES.GLOSSARY_DETAILS; + path = path.replace(PLACEHOLDER_GLOSSARY_NAME, fqn); + } + + return path; }; export const getGlossaryTermsPath = ( diff --git a/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts b/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts new file mode 100644 index 00000000000..316f4a1d4ee --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2021 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. + */ + +const jsonData = { + 'api-error-messages': { + 'add-glossary-error': 'Error while adding glossary!', + 'add-glossary-term-error': 'Error while adding glossary term!', + 'delete-glossary-error': 'Error while deleting glossary!', + 'delete-glossary-term-error': 'Error while deleting glossary term!', + 'elastic-search-error': 'Error while fetch data from Elasticsearch!', + 'fetch-data-error': 'Error while fetching data!', + 'fetch-glossary-error': 'Error while fetching glossary!', + 'fetch-glossary-list-error': 'Error while fetching glossaries!', + 'fetch-glossary-term-error': 'Error while fetching glossary term!', + 'fetch-tags-error': 'Error while fetching tags!', + 'update-glossary-term-error': 'Error while updating glossary term!', + 'update-description-error': 'Error while updating description!', + }, +}; + +export default jsonData; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddGlossary/AddGlossaryPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddGlossary/AddGlossaryPage.component.tsx index 4ff6e7ef959..8c0db900809 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddGlossary/AddGlossaryPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddGlossary/AddGlossaryPage.component.tsx @@ -6,10 +6,11 @@ import { useAuthContext } from '../../auth-provider/AuthProvider'; import { addGlossaries } from '../../axiosAPIs/glossaryAPI'; import AddGlossary from '../../components/AddGlossary/AddGlossary.component'; import PageContainerV1 from '../../components/containers/PageContainerV1'; -import { ROUTES } from '../../constants/constants'; +import { getGlossaryPath } from '../../constants/constants'; import { CreateGlossary } from '../../generated/api/data/createGlossary'; import { useAuth } from '../../hooks/authHooks'; import useToastContext from '../../hooks/useToastContext'; +import jsonData from '../../jsons/en'; import { getTagCategories, getTaglist } from '../../utils/TagsUtils'; const AddGlossaryPage: FunctionComponent = () => { @@ -21,30 +22,44 @@ const AddGlossaryPage: FunctionComponent = () => { const [isTagLoading, setIsTagLoading] = useState(false); const [status, setStatus] = useState('initial'); - const goToGlossary = () => { - history.push(ROUTES.GLOSSARY); + const goToGlossary = (name = '') => { + history.push(getGlossaryPath(name)); }; const handleCancel = () => { goToGlossary(); }; + const handleShowErrorToast = (errMessage: string) => { + showToast({ + variant: 'error', + body: errMessage, + }); + }; + + const handleSaveFailure = (errorMessage = '') => { + handleShowErrorToast( + errorMessage || jsonData['api-error-messages']['add-glossary-error'] + ); + setStatus('initial'); + }; + const onSave = (data: CreateGlossary) => { setStatus('waiting'); addGlossaries(data) - .then(() => { - setStatus('success'); - setTimeout(() => { - setStatus('initial'); - goToGlossary(); - }, 500); + .then((res) => { + if (res.data) { + setStatus('success'); + setTimeout(() => { + setStatus('initial'); + goToGlossary(res.data.name); + }, 500); + } else { + handleSaveFailure(); + } }) .catch((err: AxiosError) => { - showToast({ - variant: 'error', - body: err.message || 'Something went wrong!', - }); - setStatus('initial'); + handleSaveFailure(err.response?.data?.message); }); }; @@ -52,7 +67,19 @@ const AddGlossaryPage: FunctionComponent = () => { setIsTagLoading(true); getTagCategories() .then((res) => { - setTagList(getTaglist(res.data)); + if (res.data) { + setTagList(getTaglist(res.data)); + } else { + handleShowErrorToast( + jsonData['api-error-messages']['fetch-tags-error'] + ); + } + }) + .catch((err: AxiosError) => { + handleShowErrorToast( + err.response?.data?.message || + jsonData['api-error-messages']['fetch-tags-error'] + ); }) .finally(() => { setIsTagLoading(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryPage/GlossaryPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryPage/GlossaryPage.test.tsx index 1bee956904b..f7b311addb4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryPage/GlossaryPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryPage/GlossaryPage.test.tsx @@ -1,8 +1,27 @@ import { findByText, render } from '@testing-library/react'; import React from 'react'; -import GlossaryPage from './GlossaryPage.component'; +import GlossaryPageV1 from './GlossaryPageV1.component'; -jest.mock('../../components/Glossary/Glossary.component', () => { +jest.mock('react-router-dom', () => ({ + useHistory: jest.fn(), + useParams: jest.fn().mockReturnValue({ + glossaryName: 'GlossaryName', + }), +})); + +jest.mock('../../auth-provider/AuthProvider', () => { + return { + useAuthContext: jest.fn(() => ({ + isAuthDisabled: false, + isAuthenticated: true, + isProtectedRoute: jest.fn().mockReturnValue(true), + isTourRoute: jest.fn().mockReturnValue(false), + onLogoutHandler: jest.fn(), + })), + }; +}); + +jest.mock('../../components/Glossary/GlossaryV1.component', () => { return jest.fn().mockReturnValue(
Glossary.component
); }); @@ -12,7 +31,7 @@ jest.mock('../../axiosAPIs/glossaryAPI', () => ({ describe('Test GlossaryComponent page', () => { it('GlossaryComponent Page Should render', async () => { - const { container } = render(); + const { container } = render(); const glossaryComponent = await findByText( container, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryPage/GlossaryPageV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryPage/GlossaryPageV1.component.tsx index 26e95dfb03a..5040f1de96a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryPage/GlossaryPageV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryPage/GlossaryPageV1.component.tsx @@ -13,7 +13,7 @@ import { AxiosError, AxiosResponse } from 'axios'; import { compare } from 'fast-json-patch'; -import { cloneDeep, extend } from 'lodash'; +import { cloneDeep, extend, isEmpty } from 'lodash'; import { FormattedGlossarySuggestion, GlossarySuggestionHit, @@ -22,7 +22,7 @@ import { SearchResponse, } from 'Models'; import React, { useCallback, useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useParams } from 'react-router-dom'; import { useAuthContext } from '../../auth-provider/AuthProvider'; import { deleteGlossary, @@ -38,6 +38,7 @@ import GlossaryV1 from '../../components/Glossary/GlossaryV1.component'; import Loader from '../../components/Loader/Loader'; import { getAddGlossaryTermsPath, + getGlossaryPath, PAGE_SIZE, ROUTES, } from '../../constants/constants'; @@ -47,11 +48,14 @@ import { Glossary } from '../../generated/entity/data/glossary'; import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm'; import { useAuth } from '../../hooks/authHooks'; import useToastContext from '../../hooks/useToastContext'; +import jsonData from '../../jsons/en'; import { formatDataResponse } from '../../utils/APIUtils'; import { getChildGlossaryTerms, getGlossariesWithRootTerms, getHierarchicalKeysByFQN, + getTermDataFromGlossary, + getTermPosFromGlossaries, updateGlossaryListBySearchedTerms, } from '../../utils/GlossaryUtils'; @@ -60,8 +64,7 @@ export type ModifiedGlossaryData = Glossary & { }; const GlossaryPageV1 = () => { - // const { glossaryName, glossaryTermsFQN } = - // useParams<{ [key: string]: string }>(); + const { glossaryName } = useParams>(); const { isAdminUser } = useAuth(); const { isAuthDisabled } = useAuthContext(); @@ -88,6 +91,13 @@ const GlossaryPageV1 = () => { currPage: 1, }); + const handleShowErrorToast = (errMessage: string) => { + showToast({ + variant: 'error', + body: errMessage, + }); + }; + const handleChildLoading = (status: boolean) => { setIsChildLoading(status); }; @@ -100,6 +110,16 @@ const GlossaryPageV1 = () => { setExpandedKey(key); }; + const handleSearchText = (text: string) => { + setSearchText(text); + }; + + /** + * Selects glossary after fetching list + * if no fqn is present in route params + * @param data Glossary to be selected initially + * @param noSetData bool to decide if data is already set + */ const initSelectGlossary = (data: Glossary, noSetData = false) => { if (!noSetData) { setSelectedData(data); @@ -109,47 +129,24 @@ const GlossaryPageV1 = () => { setExpandedKey([data.name]); }; - const fetchGlossaryList = (paging = '') => { - setIsLoading(true); - getGlossariesWithRootTerms(paging, 100, ['owner', 'tags', 'reviewers']) - .then((data: ModifiedGlossaryData[]) => { - if (data?.length) { - setGlossaries(data); - setGlossariesList(data); - initSelectGlossary(data[0]); - } else { - setGlossariesList([]); - } - setIsLoading(false); - }) - .catch((err: AxiosError) => { - showToast({ - variant: 'error', - body: err.response?.data?.message ?? 'Something went wrong!', - }); - setIsLoading(false); - }) - .finally(() => { - handleChildLoading(false); - }); - }; - + /** + * To fetch glossary term data + * @param fqn fullyQualifiedName of term + * @param pos hierarchical position of term in existing tree + * @param arrGlossary list of available/fetched glossaries + */ const fetchGlossaryTermByName = ( - name: string, - pos: string[], - key?: string + fqn: string, + pos: number[], + arrGlossary: ModifiedGlossaryData[] ) => { - getGlossaryTermByFQN(name, [ - 'children', - 'relatedTerms', - 'reviewers', - 'tags', - ]) + getGlossaryTermByFQN(fqn, ['children', 'relatedTerms', 'reviewers', 'tags']) .then(async (res: AxiosResponse) => { const { data } = res; if (data) { - const clonedGlossaryList = cloneDeep(glossariesList); - let treeNode = clonedGlossaryList[+pos[0]]; + const clonedGlossaryList = cloneDeep(arrGlossary); + let treeNode = clonedGlossaryList[pos[0]]; + for (let i = 1; i < pos.length; i++) { if (treeNode.children) { treeNode = treeNode.children[+pos[i]] as ModifiedGlossaryData; @@ -193,29 +190,188 @@ const GlossaryPageV1 = () => { extend(treeNode, { ...data, children }); setSelectedData(data); - if (key) { - handleSelectedKey(key); + if (fqn) { + if (!expandedKey.length) { + setExpandedKey(getHierarchicalKeysByFQN(fqn)); + } + handleSelectedKey(fqn); } setGlossariesList(clonedGlossaryList); setIsGlossaryActive(false); + } else { + handleShowErrorToast( + jsonData['api-error-messages']['fetch-glossary-term-error'] + ); } }) .catch((err: AxiosError) => { - showToast({ - variant: 'error', - body: - err.response?.data?.message ?? - 'Error while fetching glossary terms!', - }); + handleShowErrorToast( + err.response?.data?.message || + jsonData['api-error-messages']['fetch-glossary-term-error'] + ); }) .finally(() => { + setIsLoading(false); handleChildLoading(false); setLoadingKey((pre) => { - return pre.filter((item) => item !== key); + return pre.filter((item) => item !== fqn); }); }); }; + /** + * To fetch Assets using glossary term + * @param fqn fullyQualifiedName of term + * @param forceReset bool to reset the page to 1, incase of change in glossary term + */ + const fetchGlossaryTermAssets = (fqn: string, forceReset = false) => { + if (fqn) { + const tagName = fqn; + searchData( + '', + forceReset ? 1 : assetData.currPage, + PAGE_SIZE, + `(tags:"${tagName}")`, + '', + '', + myDataSearchIndex + ) + .then((res: SearchResponse) => { + const hits = res?.data?.hits?.hits; + if (hits?.length > 0) { + setAssetData((pre) => { + const data = formatDataResponse(hits); + const total = res.data.hits.total.value; + + return forceReset + ? { + data, + total, + currPage: 1, + } + : { ...pre, data, total }; + }); + } else { + setAssetData((pre) => { + const data = [] as GlossaryTermAssets['data']; + const total = 0; + + return forceReset + ? { + data, + total, + currPage: 1, + } + : { ...pre, data, total }; + }); + } + }) + .catch((err: AxiosError) => { + handleShowErrorToast( + err.response?.data?.message || + jsonData['api-error-messages']['elastic-search-error'] + ); + }); + } else { + setAssetData({ data: [], total: 0, currPage: 1 }); + } + }; + + /** + * To select data based on glossary or term name + * @param dataFQN fullyQualifiedName of glossary or term + * @param arrGlossary list of available/fetched glossaries + */ + const selectDataByFQN = ( + dataFQN: string, + arrGlossary: ModifiedGlossaryData[] + ) => { + handleChildLoading(true); + const hierarchy = getTermPosFromGlossaries(arrGlossary, dataFQN); + if (hierarchy.length < 2) { + setSelectedData(arrGlossary[hierarchy[0]]); + handleSelectedKey(dataFQN); + if (!expandedKey.length) { + setExpandedKey([dataFQN]); + } + setIsGlossaryActive(true); + setIsLoading(false); + handleChildLoading(false); + } else { + setLoadingKey((pre) => { + return !pre.includes(dataFQN) ? [...pre, dataFQN] : pre; + }); + fetchGlossaryTermByName(dataFQN, hierarchy, arrGlossary); + fetchGlossaryTermAssets(dataFQN, true); + } + }; + + /** + * To check if glossary/term already exists and add to tree if they don't + * Then select the glossary/term by it's fqn + * @param arrGlossary list of available/fetched glossaries + * @param fqn fullyQualifiedName of glossary or term + */ + const checkAndFetchDataByFQN = ( + arrGlossary: ModifiedGlossaryData[], + fqn: string + ) => { + let modifiedData = cloneDeep(arrGlossary); + const arrFQN = getHierarchicalKeysByFQN(fqn); + const glossary: ModifiedGlossaryData | GlossaryTerm = modifiedData.find( + (item) => item.name === arrFQN[0] + ) as ModifiedGlossaryData; + const data = getTermDataFromGlossary(glossary, fqn); + if (isEmpty(data)) { + modifiedData = updateGlossaryListBySearchedTerms(modifiedData, [ + { fqdn: arrFQN[arrFQN.length - 1] }, + ] as FormattedGlossarySuggestion[]); + } + selectDataByFQN(fqn, modifiedData); + }; + + /** + * To fetch the list of all glossaries, + * and check for selection if nested fqn available + * @param termFqn fullyQualifiedName of term + * @param paging cursor pagination + */ + const fetchGlossaryList = (termFqn = '', paging = '') => { + setIsLoading(true); + getGlossariesWithRootTerms(paging, 1000, ['owner', 'tags', 'reviewers']) + .then((data: ModifiedGlossaryData[]) => { + if (data?.length) { + setGlossaries(data); + setGlossariesList(data); + if (termFqn) { + checkAndFetchDataByFQN(data, termFqn); + } else { + initSelectGlossary(data[0]); + setIsLoading(false); + handleChildLoading(false); + } + } else { + setGlossariesList([]); + setIsLoading(false); + handleChildLoading(false); + } + }) + .catch((err: AxiosError) => { + handleShowErrorToast( + err.response?.data?.message || + jsonData['api-error-messages']['fetch-glossary-list-error'] + ); + setIsLoading(false); + handleChildLoading(false); + }); + }; + + /** + * To update glossary tree based on searched terms + * @param arrGlossaries list of glossaries + * @param newGlossaries set of glossaries present in searched terms + * @param searchedTerms list of formatted searched terms + */ const getSearchedGlossaries = ( arrGlossaries: ModifiedGlossaryData[], newGlossaries: string[], @@ -247,6 +403,9 @@ const GlossaryPageV1 = () => { } }; + /** + * To fetch terms based on search text + */ const fetchSearchedTerms = useCallback(() => { if (searchText) { searchData( @@ -298,6 +457,11 @@ const GlossaryPageV1 = () => { } }, [searchText]); + /** + * To save updated glossary using patch method + * @param updatedData glossary with new values + * @returns promise of api response + */ const saveUpdatedGlossaryData = ( updatedData: Glossary ): Promise => { @@ -309,6 +473,10 @@ const GlossaryPageV1 = () => { ) as unknown as Promise; }; + /** + * To update glossary + * @param updatedData glossary with new values + */ const updateGlossary = (updatedData: Glossary) => { saveUpdatedGlossaryData(updatedData) .then((res: AxiosResponse) => { @@ -337,17 +505,25 @@ const GlossaryPageV1 = () => { } }); }); + } else { + handleShowErrorToast( + jsonData['api-error-messages']['update-description-error'] + ); } }) .catch((err: AxiosError) => { - showToast({ - variant: 'error', - body: - err.response?.data?.message ?? 'Error while updating description!', - }); + handleShowErrorToast( + err.response?.data?.message || + jsonData['api-error-messages']['update-description-error'] + ); }); }; + /** + * To save updated glossary term using patch method + * @param updatedData glossary term with new values + * @returns promise of api response + */ const saveUpdatedGlossaryTermData = ( updatedData: GlossaryTerm ): Promise => { @@ -359,20 +535,33 @@ const GlossaryPageV1 = () => { ) as unknown as Promise; }; + /** + * To update glossary term + * @param updatedData glossary term with new values + */ const handleGlossaryTermUpdate = (updatedData: GlossaryTerm) => { saveUpdatedGlossaryTermData(updatedData) .then((res: AxiosResponse) => { - setSelectedData(res.data); + if (res.data) { + setSelectedData(res.data); + } else { + handleShowErrorToast( + jsonData['api-error-messages']['update-glossary-term-error'] + ); + } }) .catch((err: AxiosError) => { - showToast({ - variant: 'error', - body: - err.response?.data?.message ?? 'Error while updating glossaryTerm!', - }); + handleShowErrorToast( + err.response?.data?.message || + jsonData['api-error-messages']['update-glossary-term-error'] + ); }); }; + /** + * To delete glossary by id + * @param id glossary id + */ const handleGlossaryDelete = (id: string) => { setDeleteStatus('waiting'); deleteGlossary(id) @@ -381,14 +570,18 @@ const GlossaryPageV1 = () => { fetchGlossaryList(); }) .catch((err: AxiosError) => { - showToast({ - variant: 'error', - body: err.response?.data?.message ?? 'Something went wrong!', - }); + handleShowErrorToast( + err.response?.data?.message || + jsonData['api-error-messages']['delete-glossary-error'] + ); setDeleteStatus('initial'); }); }; + /** + * To delete glossary term by id + * @param id glossary term id + */ const handleGlossaryTermDelete = (id: string) => { setDeleteStatus('waiting'); deleteGlossaryTerm(id) @@ -397,18 +590,24 @@ const GlossaryPageV1 = () => { fetchGlossaryList(); }) .catch((err: AxiosError) => { - showToast({ - variant: 'error', - body: err.response?.data?.message ?? 'Something went wrong!', - }); + handleShowErrorToast( + err.response?.data?.message || + jsonData['api-error-messages']['delete-glossary-term-error'] + ); setDeleteStatus('initial'); }); }; + /** + * To redirect to add glossary page + */ const handleAddGlossaryClick = () => { history.push(ROUTES.ADD_GLOSSARY); }; + /** + * To redirct to add glossary term page + */ const handleAddGlossaryTermClick = () => { const activeTerm = selectedKey.split('.'); const glossaryName = activeTerm[0]; @@ -419,88 +618,39 @@ const GlossaryPageV1 = () => { } }; - const fetchGlossaryTermAssets = (data: GlossaryTerm, forceReset = false) => { - if (data?.fullyQualifiedName || data?.name) { - const tagName = data?.fullyQualifiedName || data?.name; // Incase fqn is not fetched yet. - searchData( - '', - forceReset ? 1 : assetData.currPage, - PAGE_SIZE, - `(tags:"${tagName}")`, - '', - '', - myDataSearchIndex - ).then((res: SearchResponse) => { - const hits = res.data.hits.hits; - if (hits.length > 0) { - setAssetData((pre) => { - const data = formatDataResponse(hits); - const total = res.data.hits.total.value; - - return forceReset - ? { - data, - total, - currPage: 1, - } - : { ...pre, data, total }; - }); - } else { - setAssetData((pre) => { - const data = [] as GlossaryTermAssets['data']; - const total = 0; - - return forceReset - ? { - data, - total, - currPage: 1, - } - : { ...pre, data, total }; - }); - } - }); - } else { - setAssetData({ data: [], total: 0, currPage: 1 }); - } - }; - + /** + * handle assets page change + * @param page new page number + */ const handleAssetPagination = (page: number) => { setAssetData((pre) => ({ ...pre, currPage: page })); }; - const handleSelectedData = ( - data: Glossary | GlossaryTerm, - pos: string, - key: string - ) => { - handleChildLoading(true); - const hierarchy = pos.split('-').splice(1); - // console.log(hierarchy); - if (hierarchy.length < 2) { - setSelectedData(data); - handleSelectedKey(key); - setIsGlossaryActive(true); - handleChildLoading(false); + /** + * handle route change on selecting glossary or glossary term + * @param key fqn of glossary or Term + */ + const handleSelectedData = (key: string) => { + const path = getGlossaryPath(key); + history.push(path); + }; + + /** + * Fetch details to show based on route params + * and existing data list + */ + const fetchData = () => { + if (glossariesList.length) { + checkAndFetchDataByFQN(glossariesList, glossaryName); } else { - setLoadingKey((pre) => { - return !pre.includes(key) ? [...pre, key] : pre; - }); - fetchGlossaryTermByName( - (data as GlossaryTerm)?.fullyQualifiedName || data?.name, - hierarchy, - key - ); - fetchGlossaryTermAssets(data as GlossaryTerm, true); + fetchGlossaryList(glossaryName); } }; - const handleSearchText = (text: string) => { - setSearchText(text); - }; - useEffect(() => { - fetchGlossaryTermAssets(selectedData as GlossaryTerm); + fetchGlossaryTermAssets( + (selectedData as GlossaryTerm)?.fullyQualifiedName || '' + ); }, [assetData.currPage]); useEffect(() => { @@ -508,8 +658,8 @@ const GlossaryPageV1 = () => { }, [searchText]); useEffect(() => { - fetchGlossaryList(); - }, []); + fetchData(); + }, [glossaryName]); return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.ts index a5cf54cac7d..d5e0b504c44 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.ts @@ -12,6 +12,7 @@ */ import { AxiosError, AxiosResponse } from 'axios'; +import { cloneDeep, isEmpty } from 'lodash'; import { FormattedGlossarySuggestion, FormattedGlossaryTermData, @@ -37,6 +38,10 @@ export interface GlossaryTermTreeNode { name: string; } +/** + * To get all glossary terms + * @returns promise of list of formatted glossary terms + */ export const fetchGlossaryTerms = (): Promise => { return new Promise((resolve, reject) => { searchData(WILD_CARD_CHAR, 1, 1000, '', '', '', SearchIndex.GLOSSARY) @@ -50,12 +55,22 @@ export const fetchGlossaryTerms = (): Promise => { }); }; +/** + * To get list of fqns from list of glossary terms + * @param terms formatted glossary terms + * @returns list of term fqns + */ export const getGlossaryTermlist = ( terms: Array = [] ): Array => { return terms.map((term: FormattedGlossaryTermData) => term?.fqdn); }; +/** + * To get child terms of any node if available + * @param listTermFQN fqn of targeted child terms + * @returns promise of list of glossary terms + */ export const getChildGlossaryTerms = ( listTermFQN: Array ): Promise => { @@ -76,6 +91,11 @@ export const getChildGlossaryTerms = ( }); }; +/** + * To recursively generate RcTree data from glossary list + * @param data list of glossary or glossary terms + * @returns RcTree data node + */ export const generateTreeData = (data: ModifiedGlossaryData[]): DataNode[] => { return data.map((d) => { return d.children?.length @@ -93,6 +113,13 @@ export const generateTreeData = (data: ModifiedGlossaryData[]): DataNode[] => { }); }; +/** + * Creates glossary term tree node from fqn + * and root node name + * @param leafFqn node fqn + * @param name root node name + * @returns node for glossary tree + */ const createGlossaryTermNode = ( leafFqn: string, name: string @@ -112,6 +139,12 @@ const createGlossaryTermNode = ( }; }; +/** + * To merge the duplicate glossaries and terms + * to generate optimised tree + * @param treeNodes list of glossary nodes with duplicate items + * @returns list of glossary nodes with unique items + */ const optimiseGlossaryTermTree = (treeNodes?: GlossaryTermTreeNode[]) => { if (treeNodes) { for (let i = 0; i < treeNodes.length; i++) { @@ -139,6 +172,11 @@ const optimiseGlossaryTermTree = (treeNodes?: GlossaryTermTreeNode[]) => { return treeNodes; }; +/** + * To generate glossry tree from searched terms + * @param searchedTerms list of formatted searched terms + * @returns list of glossary tree + */ export const getSearchedGlossaryTermTree = ( searchedTerms: FormattedGlossarySuggestion[] ): GlossaryTermTreeNode[] => { @@ -153,6 +191,12 @@ export const getSearchedGlossaryTermTree = ( return termTree; }; +/** + * To get Tree of glossaries based on search result + * @param glossaries list of glossaries + * @param searchedTerms list of formatted searched terms + * @returns glossary list based on searched terms + */ export const updateGlossaryListBySearchedTerms = ( glossaries: ModifiedGlossaryData[], searchedTerms: FormattedGlossarySuggestion[] @@ -171,6 +215,10 @@ export const updateGlossaryListBySearchedTerms = ( }, [] as ModifiedGlossaryData[]); }; +/** + * To get actions for action dropdown button + * @returns list of action items + */ export const getActionsList = () => { return [ { @@ -180,6 +228,12 @@ export const getActionsList = () => { ]; }; +/** + * To get hierarchy of fqns from glossary to targeted term + * from given fqn + * @param fqn fqn of glossary or glossary term + * @returns list of fqns + */ export const getHierarchicalKeysByFQN = (fqn: string) => { const keys = fqn.split('.').reduce((prev, curr) => { const currFqn = prev.length ? `${prev[prev.length - 1]}.${curr}` : curr; @@ -190,12 +244,80 @@ export const getHierarchicalKeysByFQN = (fqn: string) => { return keys; }; +/** + * To get glossary term data from glossary object + * @param glossary parent glossary + * @param termFqn fqn of targeted glossary term + * @returns Glossary term or {} + */ +export const getTermDataFromGlossary = ( + glossary: ModifiedGlossaryData, + termFqn: string +) => { + let data: ModifiedGlossaryData | GlossaryTerm = cloneDeep(glossary); + const arrFQN = getHierarchicalKeysByFQN(termFqn); + for (let i = 1; i < arrFQN.length; i++) { + data = data?.children + ? ((data.children as unknown as GlossaryTerm[])?.find( + (item) => + item.fullyQualifiedName === arrFQN[i] || item.name === arrFQN[i] + ) as GlossaryTerm) + : ({} as GlossaryTerm); + if (isEmpty(data)) { + break; + } + } + + return data; +}; + +/** + * To get relative indexed position of + * glossary term from tree of glossaries + * @param arrGlossary list of glossary + * @param termFqn fqn of target glossary term + * @returns array of numbered positions + */ +export const getTermPosFromGlossaries = ( + arrGlossary: ModifiedGlossaryData[], + termFqn: string +) => { + const arrFQN = getHierarchicalKeysByFQN(termFqn); + const glossaryIdx = arrGlossary.findIndex((item) => item.name === arrFQN[0]); + const pos = []; + if (glossaryIdx !== -1) { + pos.push(glossaryIdx); + let data: ModifiedGlossaryData | GlossaryTerm = arrGlossary[glossaryIdx]; + for (let i = 1; i < arrFQN.length; i++) { + const index = data?.children + ? (data.children as unknown as GlossaryTerm[])?.findIndex( + (item) => + item.fullyQualifiedName === arrFQN[i] || item.name === arrFQN[i] + ) + : -1; + + if (index === -1) { + break; + } + data = (data?.children ? data?.children[index] : {}) as GlossaryTerm; + pos.push(index); + } + } + + return pos; +}; + +/** + * Fetches and adds root terms to each glossary + * @param glossaries list of glossaries + * @returns promise of list of glossaries with root terms + */ const getRootTermEmbeddedGlossary = ( glossaries: Array ): Promise> => { return new Promise>((resolve, reject) => { const promises = glossaries.map((glossary) => - getGlossaryTerms(glossary.id, 100, [ + getGlossaryTerms(glossary.id, 1000, [ 'children', 'relatedTerms', 'reviewers', @@ -222,6 +344,13 @@ const getRootTermEmbeddedGlossary = ( }); }; +/** + * Fetches list of glossaries with root terms in each of them + * @param paging pagination cursor + * @param limit result count + * @param arrQueryFields api query-string + * @returns promise of api response + */ export const getGlossariesWithRootTerms = ( paging = '', limit = 10,