Feat: Change UI of ES error placeholder (#1044)

* Feat: issue-944 add documentation guid for user when ES disconnected and index is missing

* change miner UI of error placeholder

* fixed failing test

* rename entityList to entityIndexList

* show full screen error placeholder on index missing in explore page
This commit is contained in:
Shailesh Parmar 2021-11-08 13:37:13 +05:30 committed by GitHub
parent 7771e42e1b
commit e1d7260b3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 209 additions and 123 deletions

View File

@ -112,6 +112,7 @@ const Explore: React.FC<ExploreProps> = ({
const [fieldList, setFieldList] = const [fieldList, setFieldList] =
useState<Array<{ name: string; value: string }>>(tableSortingFields); useState<Array<{ name: string; value: string }>>(tableSortingFields);
const [isEntityLoading, setIsEntityLoading] = useState(true); const [isEntityLoading, setIsEntityLoading] = useState(true);
const [connectionError] = useState(error.includes('Connection refused'));
const isMounting = useRef(true); const isMounting = useRef(true);
const forceSetAgg = useRef(false); const forceSetAgg = useRef(false);
const previsouIndex = usePrevious(searchIndex); const previsouIndex = usePrevious(searchIndex);
@ -503,9 +504,9 @@ const Explore: React.FC<ExploreProps> = ({
}; };
return ( return (
<PageContainer leftPanelContent={fetchLeftPanel()}> <PageContainer leftPanelContent={Boolean(!error) && fetchLeftPanel()}>
<div className="container-fluid" data-testid="fluid-container"> <div className="container-fluid" data-testid="fluid-container">
{getTabs()} {!connectionError && getTabs()}
{error ? ( {error ? (
<ErrorPlaceHolderES errorMessage={error} type="error" /> <ErrorPlaceHolderES errorMessage={error} type="error" />
) : ( ) : (

View File

@ -21,6 +21,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { Ownership } from '../../enums/mydata.enum'; import { Ownership } from '../../enums/mydata.enum';
import { formatDataResponse } from '../../utils/APIUtils'; import { formatDataResponse } from '../../utils/APIUtils';
import { getCurrentUserId } from '../../utils/CommonUtils'; import { getCurrentUserId } from '../../utils/CommonUtils';
import ErrorPlaceHolderES from '../common/error-with-placeholder/ErrorPlaceHolderES';
import PageContainer from '../containers/PageContainer'; import PageContainer from '../containers/PageContainer';
import MyDataHeader from '../MyDataHeader/MyDataHeader.component'; import MyDataHeader from '../MyDataHeader/MyDataHeader.component';
import RecentlyViewed from '../recently-viewed/RecentlyViewed'; import RecentlyViewed from '../recently-viewed/RecentlyViewed';
@ -28,8 +29,11 @@ import SearchedData from '../searched-data/SearchedData';
import { MyDataProps } from './MyData.interface'; import { MyDataProps } from './MyData.interface';
const MyData: React.FC<MyDataProps> = ({ const MyData: React.FC<MyDataProps> = ({
error,
errorHandler,
countServices, countServices,
userDetails, userDetails,
rejectedResult,
searchResult, searchResult,
fetchData, fetchData,
entityCounts, entityCounts,
@ -104,7 +108,7 @@ const MyData: React.FC<MyDataProps> = ({
}; };
useEffect(() => { useEffect(() => {
if (isMounted.current) { if (isMounted.current && Boolean(currentTab === 2 || currentTab === 3)) {
setIsEntityLoading(true); setIsEntityLoading(true);
fetchData({ fetchData({
queryString: '', queryString: '',
@ -118,14 +122,18 @@ const MyData: React.FC<MyDataProps> = ({
useEffect(() => { useEffect(() => {
if (searchResult) { if (searchResult) {
const hits = searchResult.data.hits.hits; const formatedData: Array<FormatedTableData> = [];
if (hits.length > 0) { let totalValue = 0;
setTotalNumberOfValues(searchResult.data.hits.total.value); searchResult.forEach((res) => {
setData(formatDataResponse(hits)); totalValue = totalValue + res.data.hits.total.value;
} else { formatedData.push(...formatDataResponse(res.data.hits.hits));
setData([]); });
setTotalNumberOfValues(0);
if (formatedData.length === 0 && rejectedResult.length > 0) {
errorHandler(rejectedResult[0].response?.data?.responseMessage);
} }
setTotalNumberOfValues(totalValue);
setData(formatedData);
} }
setIsEntityLoading(false); setIsEntityLoading(false);
}, [searchResult]); }, [searchResult]);
@ -142,11 +150,14 @@ const MyData: React.FC<MyDataProps> = ({
entityCounts={entityCounts} entityCounts={entityCounts}
/> />
{getTabs()} {getTabs()}
{error && Boolean(currentTab === 2 || currentTab === 3) ? (
<ErrorPlaceHolderES errorMessage={error} type="error" />
) : (
<SearchedData <SearchedData
showOnboardingTemplate showOnboardingTemplate
currentPage={currentPage} currentPage={currentPage}
data={data} data={data}
isLoading={isEntityLoading} isLoading={currentTab === 1 ? false : isEntityLoading}
paginate={paginate} paginate={paginate}
searchText="*" searchText="*"
showOnlyChildren={currentTab === 1} showOnlyChildren={currentTab === 1}
@ -154,6 +165,7 @@ const MyData: React.FC<MyDataProps> = ({
totalValue={totalNumberOfValue}> totalValue={totalNumberOfValue}>
{currentTab === 1 ? <RecentlyViewed /> : null} {currentTab === 1 ? <RecentlyViewed /> : null}
</SearchedData> </SearchedData>
)}
</div> </div>
</PageContainer> </PageContainer>
); );

View File

@ -2,9 +2,12 @@ import { EntityCounts, SearchDataFunctionType, SearchResponse } from 'Models';
import { User } from '../../generated/entity/teams/user'; import { User } from '../../generated/entity/teams/user';
export interface MyDataProps { export interface MyDataProps {
error: string;
countServices: number; countServices: number;
userDetails: User; userDetails: User;
searchResult: SearchResponse | undefined; rejectedResult: PromiseRejectedResult['reason'][];
errorHandler: (error: string) => void;
searchResult: SearchResponse[] | undefined;
fetchData: (value: SearchDataFunctionType) => void; fetchData: (value: SearchDataFunctionType) => void;
entityCounts: EntityCounts; entityCounts: EntityCounts;
} }

View File

@ -252,6 +252,7 @@ jest.mock('../../utils/ServiceUtils', () => ({
})); }));
const fetchData = jest.fn(); const fetchData = jest.fn();
const errorHandler = jest.fn();
describe('Test MyData page', () => { describe('Test MyData page', () => {
it('Check if there is an element in the page', async () => { it('Check if there is an element in the page', async () => {
@ -264,8 +265,11 @@ describe('Test MyData page', () => {
dashboardCount: 8, dashboardCount: 8,
pipelineCount: 1, pipelineCount: 1,
}} }}
error=""
errorHandler={errorHandler}
fetchData={fetchData} fetchData={fetchData}
searchResult={mockData as unknown as SearchResponse} rejectedResult={[]}
searchResult={[mockData] as unknown as SearchResponse[]}
userDetails={mockUserDetails as unknown as User} userDetails={mockUserDetails as unknown as User}
/>, />,
{ {

View File

@ -1,7 +1,7 @@
import { uniqueId } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React from 'react'; import React from 'react';
import AppState from '../../../AppState'; import AppState from '../../../AppState';
import NoDataFoundPlaceHolder from '../../../assets/img/no-data-placeholder.png';
import { useAuth } from '../../../hooks/authHooks'; import { useAuth } from '../../../hooks/authHooks';
type Props = { type Props = {
@ -10,6 +10,36 @@ type Props = {
query?: string; 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 ErrorPlaceHolderES = ({ type, errorMessage, query = '' }: Props) => {
const { isAuthDisabled } = useAuth(); const { isAuthDisabled } = useAuth();
const getUserDisplayName = () => { const getUserDisplayName = () => {
@ -35,42 +65,64 @@ const ErrorPlaceHolderES = ({ type, errorMessage, query = '' }: Props) => {
const elasticSearchError = () => { const elasticSearchError = () => {
const index = errorMessage?.split('[')[3]?.split(']')[0]; const index = errorMessage?.split('[')[3]?.split(']')[0];
const errorText = errorMessage && index ? `find ${index} in` : 'access';
return errorMessage && index ? ( return (
<p className="tw-max-w-sm tw-text-center"> <div className="tw-mb-5">
OpenMetadata requires index <div className="tw-mb-3 tw-text-center">
<span className="tw-text-primary tw-font-medium tw-mx-1"> <p>
{index} <span>Welcome to OpenMetadata. </span>
</span>{' '} {`We are unable to ${errorText} Elasticsearch for entity indexes.`}
to exist while running Elasticsearch. Please check your Elasticsearch
indexes
</p> </p>
) : (
<p className="tw-max-w-sm tw-text-center"> <p>
OpenMetadata requires Elasticsearch 7+ running and configured in Please follow the instructions here to set up Metadata ingestion and
<span className="tw-text-primary tw-font-medium tw-mx-1"> index them into Elasticsearch.
openmetadata.yaml.
</span>
Please check the configuration and make sure the Elasticsearch is
running.
</p> </p>
</div>
<div className="tw-grid tw-grid-cols-4 tw-gap-4 tw-mt-5">
{stepsData.map((data) => (
<div
className="tw-card tw-flex tw-flex-col tw-justify-between tw-p-5"
key={uniqueId()}>
<div>
<div className="tw-flex tw-mb-2">
<div className="tw-rounded-full tw-flex tw-justify-center tw-items-center tw-h-10 tw-w-10 tw-border-2 tw-border-primary tw-text-lg tw-font-bold tw-text-primary">
{data.step}
</div>
</div>
<h6
className="tw-text-base tw-text-grey-body tw-font-medium"
data-testid="service-name">
{data.title}
</h6>
<p className="tw-text-grey-body tw-pb-1 tw-text-sm tw-mb-5">
{data.description}
</p>
</div>
<p>
<a href={data.link} rel="noopener noreferrer" target="_blank">
Click here &gt;&gt;
</a>
</p>
</div>
))}
</div>
</div>
); );
}; };
return ( return (
<> <div className="tw-mt-10 tw-text-base tw-font-normal">
<div className="tw-flex tw-flex-col tw-mt-24 tw-place-items-center"> <p className="tw-text-center tw-text-lg tw-font-bold tw-mb-1 tw-text-primary">
{' '}
<img src={NoDataFoundPlaceHolder} width={200} />
</div>
<div className="tw-flex tw-flex-col tw-items-center tw-mt-10 tw-text-base tw-font-normal">
<p className="tw-text-lg tw-font-bold tw-mb-1 tw-text-primary">
{`Hi, ${getUserDisplayName()}!`} {`Hi, ${getUserDisplayName()}!`}
</p> </p>
{type === 'noData' && noRecordForES()} {type === 'noData' && noRecordForES()}
{type === 'error' && elasticSearchError()} {type === 'error' && elasticSearchError()}
</div> </div>
</>
); );
}; };

View File

@ -19,16 +19,6 @@ import { FilterObject } from 'Models';
import { getCurrentUserId } from '../utils/CommonUtils'; import { getCurrentUserId } from '../utils/CommonUtils';
import { getFilterString } from '../utils/FilterUtils'; 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 = [ export const myDataFilterType = [
{ key: 'Owned', value: 'owner' }, { key: 'Owned', value: 'owner' },
{ key: 'Following', value: 'followers' }, { key: 'Following', value: 'followers' },

View File

@ -15,36 +15,50 @@
* limitations under the License. * limitations under the License.
*/ */
import { AxiosError } from 'axios';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import { observer } from 'mobx-react'; 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 React, { useEffect, useState } from 'react';
import AppState from '../../AppState'; import AppState from '../../AppState';
import { searchData } from '../../axiosAPIs/miscAPI'; import { searchData } from '../../axiosAPIs/miscAPI';
import ErrorPlaceHolderES from '../../components/common/error-with-placeholder/ErrorPlaceHolderES';
import Loader from '../../components/Loader/Loader'; import Loader from '../../components/Loader/Loader';
import MyData from '../../components/MyData/MyData.component'; import MyData from '../../components/MyData/MyData.component';
import { ERROR500, PAGE_SIZE } from '../../constants/constants'; import { PAGE_SIZE } from '../../constants/constants';
import {
myDataEntityCounts,
myDataSearchIndex,
} from '../../constants/Mydata.constants';
import useToastContext from '../../hooks/useToastContext';
import { import {
getAllServices, getAllServices,
getEntityCountByService, getEntityCountByService,
} from '../../utils/ServiceUtils'; } from '../../utils/ServiceUtils';
const MyDataPage = () => { const MyDataPage = () => {
const showToast = useToastContext();
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [countServices, setCountServices] = useState<number>(); const [countServices, setCountServices] = useState<number>();
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [searchResult, setSearchResult] = useState<SearchResponse>(); const [searchResult, setSearchResult] = useState<SearchResponse[]>();
const [rejectedResult, setRejectedResult] = useState<
PromiseRejectedResult['reason'][]
>([]);
const [entityCounts, setEntityCounts] = useState<EntityCounts>(); const [entityCounts, setEntityCounts] = useState<EntityCounts>();
const errorHandler = (error: string) => {
setError(error);
};
const fetchData = (value: SearchDataFunctionType, fetchService = false) => { const fetchData = (value: SearchDataFunctionType, fetchService = false) => {
setError('');
const entityIndexList = [
'table_search_index',
'topic_search_index',
'dashboard_search_index',
'pipeline_search_index',
];
const entityResponse = entityIndexList.map((entity) =>
searchData( searchData(
value.queryString, value.queryString,
value.from, value.from,
@ -52,26 +66,34 @@ const MyDataPage = () => {
value.filters, value.filters,
value.sortField, value.sortField,
value.sortOrder, value.sortOrder,
myDataSearchIndex entity
)
.then((res: SearchResponse) => {
setSearchResult(res);
if (isUndefined(entityCounts)) {
setEntityCounts(
getEntityCountByService(
res.data.aggregations?.['sterms#Service']?.buckets
) )
); );
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) { if (fetchService) {
getAllServices() getAllServices()
.then((res) => setCountServices(res.length)) .then((res) => setCountServices(res.length))
@ -98,17 +120,16 @@ const MyDataPage = () => {
{!isUndefined(countServices) && {!isUndefined(countServices) &&
!isUndefined(entityCounts) && !isUndefined(entityCounts) &&
!isLoading ? ( !isLoading ? (
error ? (
<ErrorPlaceHolderES errorMessage={error} type="error" />
) : (
<MyData <MyData
countServices={countServices} countServices={countServices}
entityCounts={entityCounts} entityCounts={entityCounts}
error={error}
errorHandler={errorHandler}
fetchData={fetchData} fetchData={fetchData}
rejectedResult={rejectedResult}
searchResult={searchResult} searchResult={searchResult}
userDetails={AppState.userDetails} userDetails={AppState.userDetails}
/> />
)
) : ( ) : (
<Loader /> <Loader />
)} )}

View File

@ -26,9 +26,18 @@ jest.mock('../../components/MyData/MyData.component', () => {
}); });
jest.mock('../../axiosAPIs/miscAPI', () => ({ jest.mock('../../axiosAPIs/miscAPI', () => ({
searchData: jest searchData: jest.fn().mockImplementation(() =>
.fn() Promise.resolve({
.mockImplementation(() => Promise.resolve({ data: { hits: [] } })), data: {
aggregations: {
'sterms#Service': {
buckets: [],
},
},
hits: [],
},
})
),
})); }));
jest.mock('../../utils/ServiceUtils', () => ({ jest.mock('../../utils/ServiceUtils', () => ({

View File

@ -27,7 +27,7 @@ import {
UrlParams, UrlParams,
} from '../../components/Explore/explore.interface'; } from '../../components/Explore/explore.interface';
import Loader from '../../components/Loader/Loader'; import Loader from '../../components/Loader/Loader';
import { ERROR500, PAGE_SIZE } from '../../constants/constants'; import { PAGE_SIZE } from '../../constants/constants';
import { import {
emptyValue, emptyValue,
getCurrentIndex, getCurrentIndex,
@ -39,11 +39,9 @@ import {
ZERO_SIZE, ZERO_SIZE,
} from '../../constants/explore.constants'; } from '../../constants/explore.constants';
import { SearchIndex } from '../../enums/search.enum'; import { SearchIndex } from '../../enums/search.enum';
import useToastContext from '../../hooks/useToastContext';
import { getTotalEntityCountByService } from '../../utils/ServiceUtils'; import { getTotalEntityCountByService } from '../../utils/ServiceUtils';
const ExplorePage: FunctionComponent = () => { const ExplorePage: FunctionComponent = () => {
const showToast = useToastContext();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isLoadingForData, setIsLoadingForData] = useState(true); const [isLoadingForData, setIsLoadingForData] = useState(true);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
@ -175,10 +173,6 @@ const ExplorePage: FunctionComponent = () => {
) )
.catch((err: AxiosError) => { .catch((err: AxiosError) => {
setError(err.response?.data?.responseMessage); setError(err.response?.data?.responseMessage);
showToast({
variant: 'error',
body: err.response?.data?.responseMessage ?? ERROR500,
});
setIsLoadingForData(false); setIsLoadingForData(false);
}); });
}; };

View File

@ -236,7 +236,7 @@
} }
.side-panel { .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, .side-panel .seperator,
.horz-separator { .horz-separator {