diff --git a/catalog-rest-service/src/main/resources/ui/src/components/Explore/Explore.component.tsx b/catalog-rest-service/src/main/resources/ui/src/components/Explore/Explore.component.tsx index bac0467ea99..834dfa969f6 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/Explore/Explore.component.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/Explore/Explore.component.tsx @@ -112,6 +112,7 @@ const Explore: React.FC = ({ const [fieldList, setFieldList] = useState>(tableSortingFields); const [isEntityLoading, setIsEntityLoading] = useState(true); + const [connectionError] = useState(error.includes('Connection refused')); const isMounting = useRef(true); const forceSetAgg = useRef(false); const previsouIndex = usePrevious(searchIndex); @@ -503,9 +504,9 @@ const Explore: React.FC = ({ }; return ( - +
- {getTabs()} + {!connectionError && getTabs()} {error ? ( ) : ( diff --git a/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.component.tsx b/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.component.tsx index 0bf4f71c793..c86a1ba55b1 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.component.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.component.tsx @@ -21,6 +21,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { Ownership } from '../../enums/mydata.enum'; import { formatDataResponse } from '../../utils/APIUtils'; import { getCurrentUserId } from '../../utils/CommonUtils'; +import ErrorPlaceHolderES from '../common/error-with-placeholder/ErrorPlaceHolderES'; import PageContainer from '../containers/PageContainer'; import MyDataHeader from '../MyDataHeader/MyDataHeader.component'; import RecentlyViewed from '../recently-viewed/RecentlyViewed'; @@ -28,8 +29,11 @@ import SearchedData from '../searched-data/SearchedData'; import { MyDataProps } from './MyData.interface'; const MyData: React.FC = ({ + error, + errorHandler, countServices, userDetails, + rejectedResult, searchResult, fetchData, entityCounts, @@ -104,7 +108,7 @@ const MyData: React.FC = ({ }; useEffect(() => { - if (isMounted.current) { + if (isMounted.current && Boolean(currentTab === 2 || currentTab === 3)) { setIsEntityLoading(true); fetchData({ queryString: '', @@ -118,14 +122,18 @@ const MyData: React.FC = ({ useEffect(() => { if (searchResult) { - const hits = searchResult.data.hits.hits; - if (hits.length > 0) { - setTotalNumberOfValues(searchResult.data.hits.total.value); - setData(formatDataResponse(hits)); - } else { - setData([]); - setTotalNumberOfValues(0); + const formatedData: Array = []; + let totalValue = 0; + searchResult.forEach((res) => { + totalValue = totalValue + res.data.hits.total.value; + formatedData.push(...formatDataResponse(res.data.hits.hits)); + }); + + if (formatedData.length === 0 && rejectedResult.length > 0) { + errorHandler(rejectedResult[0].response?.data?.responseMessage); } + setTotalNumberOfValues(totalValue); + setData(formatedData); } setIsEntityLoading(false); }, [searchResult]); @@ -142,18 +150,22 @@ const MyData: React.FC = ({ entityCounts={entityCounts} /> {getTabs()} - 0 ? true : false} - totalValue={totalNumberOfValue}> - {currentTab === 1 ? : null} - + {error && Boolean(currentTab === 2 || currentTab === 3) ? ( + + ) : ( + 0 ? true : false} + totalValue={totalNumberOfValue}> + {currentTab === 1 ? : null} + + )}
); diff --git a/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.interface.ts b/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.interface.ts index f70c2dbff3f..9deef41ffd3 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.interface.ts +++ b/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.interface.ts @@ -2,9 +2,12 @@ import { EntityCounts, SearchDataFunctionType, SearchResponse } from 'Models'; import { User } from '../../generated/entity/teams/user'; export interface MyDataProps { + error: string; countServices: number; userDetails: User; - searchResult: SearchResponse | undefined; + rejectedResult: PromiseRejectedResult['reason'][]; + errorHandler: (error: string) => void; + searchResult: SearchResponse[] | undefined; fetchData: (value: SearchDataFunctionType) => void; entityCounts: EntityCounts; } diff --git a/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.test.tsx b/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.test.tsx index efd64b70426..1b7449e3ea5 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.test.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/MyData/MyData.test.tsx @@ -252,6 +252,7 @@ jest.mock('../../utils/ServiceUtils', () => ({ })); const fetchData = jest.fn(); +const errorHandler = jest.fn(); describe('Test MyData page', () => { it('Check if there is an element in the page', async () => { @@ -264,8 +265,11 @@ describe('Test MyData page', () => { dashboardCount: 8, pipelineCount: 1, }} + error="" + errorHandler={errorHandler} fetchData={fetchData} - searchResult={mockData as unknown as SearchResponse} + rejectedResult={[]} + searchResult={[mockData] as unknown as SearchResponse[]} userDetails={mockUserDetails as unknown as User} />, { diff --git a/catalog-rest-service/src/main/resources/ui/src/components/common/error-with-placeholder/ErrorPlaceHolderES.tsx b/catalog-rest-service/src/main/resources/ui/src/components/common/error-with-placeholder/ErrorPlaceHolderES.tsx index d000780db01..96948ffb63d 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/common/error-with-placeholder/ErrorPlaceHolderES.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/common/error-with-placeholder/ErrorPlaceHolderES.tsx @@ -1,7 +1,7 @@ +import { uniqueId } from 'lodash'; import { observer } from 'mobx-react'; import React from 'react'; import AppState from '../../../AppState'; -import NoDataFoundPlaceHolder from '../../../assets/img/no-data-placeholder.png'; import { useAuth } from '../../../hooks/authHooks'; type Props = { @@ -10,6 +10,36 @@ type Props = { query?: string; }; +const stepsData = [ + { + step: 1, + title: 'Ingest Sample Data', + description: + 'Run sample data to ingest sample entities into your OpenMetadata', + link: 'https://docs.open-metadata.org/install/metadata-ingestion/ingest-sample-data', + }, + { + step: 2, + title: 'Start Elasticsearch Docker', + description: 'Run ingestion to index entities into OpenMetadata', + link: 'https://docs.open-metadata.org/install/metadata-ingestion/ingest-sample-data#index-sample-data-into-elasticsearch', + }, + { + step: 3, + title: 'Install Service Connectors', + description: + 'There are a lot of connectors available here to index data from your services. Please checkout our connectors', + link: 'https://docs.open-metadata.org/install/metadata-ingestion/connectors', + }, + { + step: 4, + title: 'More Help', + description: + 'If you are still running into issues, please reach out to us on slack', + link: 'https://slack.open-metadata.org', + }, +]; + const ErrorPlaceHolderES = ({ type, errorMessage, query = '' }: Props) => { const { isAuthDisabled } = useAuth(); const getUserDisplayName = () => { @@ -35,42 +65,64 @@ const ErrorPlaceHolderES = ({ type, errorMessage, query = '' }: Props) => { const elasticSearchError = () => { const index = errorMessage?.split('[')[3]?.split(']')[0]; + const errorText = errorMessage && index ? `find ${index} in` : 'access'; - return errorMessage && index ? ( -

- OpenMetadata requires index - - {index} - {' '} - to exist while running Elasticsearch. Please check your Elasticsearch - indexes -

- ) : ( -

- OpenMetadata requires Elasticsearch 7+ running and configured in - - openmetadata.yaml. - - Please check the configuration and make sure the Elasticsearch is - running. -

+ return ( +
+
+

+ Welcome to OpenMetadata. + {`We are unable to ${errorText} Elasticsearch for entity indexes.`} +

+ +

+ Please follow the instructions here to set up Metadata ingestion and + index them into Elasticsearch. +

+
+
+ {stepsData.map((data) => ( +
+
+
+
+ {data.step} +
+
+ +
+ {data.title} +
+ +

+ {data.description} +

+
+ +

+ + Click here >> + +

+
+ ))} +
+
); }; return ( - <> -
- {' '} - -
-
-

- {`Hi, ${getUserDisplayName()}!`} -

- {type === 'noData' && noRecordForES()} - {type === 'error' && elasticSearchError()} -
- +
+

+ {`Hi, ${getUserDisplayName()}!`} +

+ {type === 'noData' && noRecordForES()} + {type === 'error' && elasticSearchError()} +
); }; diff --git a/catalog-rest-service/src/main/resources/ui/src/constants/Mydata.constants.ts b/catalog-rest-service/src/main/resources/ui/src/constants/Mydata.constants.ts index 1091d9c4534..4fdb406b5d8 100644 --- a/catalog-rest-service/src/main/resources/ui/src/constants/Mydata.constants.ts +++ b/catalog-rest-service/src/main/resources/ui/src/constants/Mydata.constants.ts @@ -19,16 +19,6 @@ import { FilterObject } from 'Models'; import { getCurrentUserId } from '../utils/CommonUtils'; import { getFilterString } from '../utils/FilterUtils'; -export const myDataSearchIndex = - 'dashboard_search_index,topic_search_index,table_search_index,pipeline_search_index'; - -export const myDataEntityCounts = { - tableCount: 0, - topicCount: 0, - dashboardCount: 0, - pipelineCount: 0, -}; - export const myDataFilterType = [ { key: 'Owned', value: 'owner' }, { key: 'Following', value: 'followers' }, diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx index 4c988854d8f..0d58295d96e 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx @@ -15,63 +15,85 @@ * limitations under the License. */ -import { AxiosError } from 'axios'; import { isUndefined } from 'lodash'; import { observer } from 'mobx-react'; -import { EntityCounts, SearchDataFunctionType, SearchResponse } from 'Models'; +import { + Bucket, + EntityCounts, + SearchDataFunctionType, + SearchResponse, +} from 'Models'; import React, { useEffect, useState } from 'react'; import AppState from '../../AppState'; import { searchData } from '../../axiosAPIs/miscAPI'; -import ErrorPlaceHolderES from '../../components/common/error-with-placeholder/ErrorPlaceHolderES'; import Loader from '../../components/Loader/Loader'; import MyData from '../../components/MyData/MyData.component'; -import { ERROR500, PAGE_SIZE } from '../../constants/constants'; -import { - myDataEntityCounts, - myDataSearchIndex, -} from '../../constants/Mydata.constants'; -import useToastContext from '../../hooks/useToastContext'; +import { PAGE_SIZE } from '../../constants/constants'; import { getAllServices, getEntityCountByService, } from '../../utils/ServiceUtils'; const MyDataPage = () => { - const showToast = useToastContext(); const [error, setError] = useState(''); const [countServices, setCountServices] = useState(); const [isLoading, setIsLoading] = useState(true); - const [searchResult, setSearchResult] = useState(); + const [searchResult, setSearchResult] = useState(); + const [rejectedResult, setRejectedResult] = useState< + PromiseRejectedResult['reason'][] + >([]); const [entityCounts, setEntityCounts] = useState(); + const errorHandler = (error: string) => { + setError(error); + }; + const fetchData = (value: SearchDataFunctionType, fetchService = false) => { - searchData( - value.queryString, - value.from, - PAGE_SIZE, - value.filters, - value.sortField, - value.sortOrder, - myDataSearchIndex - ) - .then((res: SearchResponse) => { - setSearchResult(res); - if (isUndefined(entityCounts)) { - setEntityCounts( - getEntityCountByService( - res.data.aggregations?.['sterms#Service']?.buckets - ) + setError(''); + + const entityIndexList = [ + 'table_search_index', + 'topic_search_index', + 'dashboard_search_index', + 'pipeline_search_index', + ]; + + const entityResponse = entityIndexList.map((entity) => + searchData( + value.queryString, + value.from, + PAGE_SIZE, + value.filters, + value.sortField, + value.sortOrder, + entity + ) + ); + + Promise.allSettled(entityResponse).then((response) => { + const fulfilledRes: SearchResponse[] = []; + const aggregations: Bucket[] = []; + const rejectedRes: PromiseRejectedResult['reason'][] = []; + + response.forEach((entity) => { + if (entity.status === 'fulfilled') { + fulfilledRes.push(entity.value); + aggregations.push( + ...entity.value.data.aggregations?.['sterms#Service']?.buckets ); + } else { + rejectedRes.push(entity.reason); } - }) - .catch((err: AxiosError) => { - setError(err.response?.data?.responseMessage); - showToast({ - variant: 'error', - body: err.response?.data?.responseMessage ?? ERROR500, - }); - setEntityCounts(myDataEntityCounts); }); + + if (fulfilledRes.length === 0 && response[0].status === 'rejected') { + setError(response[0].reason.response?.data?.responseMessage); + } + setRejectedResult(rejectedRes); + setEntityCounts(getEntityCountByService(aggregations)); + setSearchResult(fulfilledRes as unknown as SearchResponse[]); + }); + if (fetchService) { getAllServices() .then((res) => setCountServices(res.length)) @@ -98,17 +120,16 @@ const MyDataPage = () => { {!isUndefined(countServices) && !isUndefined(entityCounts) && !isLoading ? ( - error ? ( - - ) : ( - - ) + ) : ( )} diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.test.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.test.tsx index c96b56680a1..3336159c377 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.test.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.test.tsx @@ -26,9 +26,18 @@ jest.mock('../../components/MyData/MyData.component', () => { }); jest.mock('../../axiosAPIs/miscAPI', () => ({ - searchData: jest - .fn() - .mockImplementation(() => Promise.resolve({ data: { hits: [] } })), + searchData: jest.fn().mockImplementation(() => + Promise.resolve({ + data: { + aggregations: { + 'sterms#Service': { + buckets: [], + }, + }, + hits: [], + }, + }) + ), })); jest.mock('../../utils/ServiceUtils', () => ({ diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/explore/ExplorePage.component.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/explore/ExplorePage.component.tsx index a5bf1df3e71..e73923f72c2 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/explore/ExplorePage.component.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/explore/ExplorePage.component.tsx @@ -27,7 +27,7 @@ import { UrlParams, } from '../../components/Explore/explore.interface'; import Loader from '../../components/Loader/Loader'; -import { ERROR500, PAGE_SIZE } from '../../constants/constants'; +import { PAGE_SIZE } from '../../constants/constants'; import { emptyValue, getCurrentIndex, @@ -39,11 +39,9 @@ import { ZERO_SIZE, } from '../../constants/explore.constants'; import { SearchIndex } from '../../enums/search.enum'; -import useToastContext from '../../hooks/useToastContext'; import { getTotalEntityCountByService } from '../../utils/ServiceUtils'; const ExplorePage: FunctionComponent = () => { - const showToast = useToastContext(); const [isLoading, setIsLoading] = useState(true); const [isLoadingForData, setIsLoadingForData] = useState(true); const [error, setError] = useState(''); @@ -175,10 +173,6 @@ const ExplorePage: FunctionComponent = () => { ) .catch((err: AxiosError) => { setError(err.response?.data?.responseMessage); - showToast({ - variant: 'error', - body: err.response?.data?.responseMessage ?? ERROR500, - }); setIsLoadingForData(false); }); }; diff --git a/catalog-rest-service/src/main/resources/ui/src/styles/tailwind.css b/catalog-rest-service/src/main/resources/ui/src/styles/tailwind.css index 0fe8d6ed926..39aebc634eb 100644 --- a/catalog-rest-service/src/main/resources/ui/src/styles/tailwind.css +++ b/catalog-rest-service/src/main/resources/ui/src/styles/tailwind.css @@ -236,7 +236,7 @@ } .side-panel { - @apply tw-border-r tw-border-separator; + @apply tw-border-r tw-border-separator tw-transition-all tw-duration-300; } .side-panel .seperator, .horz-separator {