ui: move queries from ES to REST API (#10947)

* initial commit for moving from ES to REST

* updated filters

* miner fix

* fixed failing unit test

* fixed failing cypress
This commit is contained in:
Shailesh Parmar 2023-04-10 10:21:27 +05:30 committed by GitHub
parent 445ac90ef9
commit d79974dba1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 148 additions and 69 deletions

View File

@ -161,11 +161,7 @@ describe('Postgres Ingestion', () => {
); );
visitEntityDetailsPage(tableName, serviceName, 'tables'); visitEntityDetailsPage(tableName, serviceName, 'tables');
verifyResponseStatusCode('@entityDetailsPage', 200); verifyResponseStatusCode('@entityDetailsPage', 200);
interceptURL( interceptURL('GET', '/api/v1/queries?*', 'queriesTab');
'GET',
'/api/v1/search/query?q=&index=query_search_index*',
'queriesTab'
);
cy.get('[data-testid="Queries"]').should('be.visible').trigger('click'); cy.get('[data-testid="Queries"]').should('be.visible').trigger('click');
verifyResponseStatusCode('@queriesTab', 200); verifyResponseStatusCode('@queriesTab', 200);
// Validate that the triggered query is visible in the queries container // Validate that the triggered query is visible in the queries container

View File

@ -15,6 +15,7 @@ import { Space, Typography } from 'antd';
import SearchDropdown from 'components/SearchDropdown/SearchDropdown'; import SearchDropdown from 'components/SearchDropdown/SearchDropdown';
import { SearchDropdownOption } from 'components/SearchDropdown/SearchDropdown.interface'; import { SearchDropdownOption } from 'components/SearchDropdown/SearchDropdown.interface';
import { WILD_CARD_CHAR } from 'constants/char.constants'; import { WILD_CARD_CHAR } from 'constants/char.constants';
import { INITIAL_PAGING_VALUE } from 'constants/constants';
import { QUERY_PAGE_FILTER } from 'constants/Query.constant'; import { QUERY_PAGE_FILTER } from 'constants/Query.constant';
import { PROMISE_STATE } from 'enums/common.enum'; import { PROMISE_STATE } from 'enums/common.enum';
import { debounce, isEmpty } from 'lodash'; import { debounce, isEmpty } from 'lodash';
@ -59,7 +60,6 @@ const QueryFilters = ({ onFilterChange }: QueryFiltersProps) => {
) => { ) => {
setSelectedFilter((pre) => { setSelectedFilter((pre) => {
const updatedFilter = { ...pre, [searchKey]: value }; const updatedFilter = { ...pre, [searchKey]: value };
onFilterChange(updatedFilter); onFilterChange(updatedFilter);
return updatedFilter; return updatedFilter;
@ -111,13 +111,13 @@ const QueryFilters = ({ onFilterChange }: QueryFiltersProps) => {
} }
}; };
const debounceOnUserSearch = debounce(onUserSearch, 400); const debounceOnUserSearch = debounce(onUserSearch, 500);
const debounceOnTeamSearch = debounce(onTeamSearch, 400); const debounceOnTeamSearch = debounce(onTeamSearch, 500);
const getInitialUserAndTeam = () => { const getInitialUserAndTeam = () => {
const promise = [ const promise = [
getSearchedUsers(WILD_CARD_CHAR, 1), getSearchedUsers(WILD_CARD_CHAR, INITIAL_PAGING_VALUE),
getSearchedTeams(WILD_CARD_CHAR, 1), getSearchedTeams(WILD_CARD_CHAR, INITIAL_PAGING_VALUE),
]; ];
Promise.allSettled(promise) Promise.allSettled(promise)
.then((res) => { .then((res) => {

View File

@ -62,10 +62,13 @@ export interface QueryFiltersProps {
onFilterChange: (value: QueryFiltersType) => void; onFilterChange: (value: QueryFiltersType) => void;
} }
export type QuerySearchParams = QueryFiltersType & { export type QuerySearchParams = {
page: string; queryFrom?: number;
tableId: string; after?: string;
query: string; tableId?: string;
query?: string;
user?: SearchDropdownOption[];
team?: SearchDropdownOption[];
}; };
export type QuerySearchShouldFilterType = { export type QuerySearchShouldFilterType = {

View File

@ -19,8 +19,8 @@ import {
} from '@testing-library/react'; } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { searchQuery } from 'rest/searchAPI'; import { getQueriesList } from 'rest/queryAPI';
import { MOCK_QUERIES_ES_DATA } from '../../mocks/Queries.mock'; import { MOCK_QUERIES, MOCK_QUERIES_ES_DATA } from '../../mocks/Queries.mock';
import TableQueries from './TableQueries'; import TableQueries from './TableQueries';
import { TableQueriesProp } from './TableQueries.interface'; import { TableQueriesProp } from './TableQueries.interface';
@ -49,6 +49,14 @@ jest.mock('rest/searchAPI', () => ({
}) })
), ),
})); }));
jest.mock('rest/queryAPI', () => ({
...jest.requireActual('rest/queryAPI'),
getQueriesList: jest
.fn()
.mockImplementation(() =>
Promise.resolve({ data: MOCK_QUERIES, paging: { total: 2 } })
),
}));
jest.mock('components/PermissionProvider/PermissionProvider', () => ({ jest.mock('components/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest usePermissionProvider: jest
.fn() .fn()
@ -89,9 +97,10 @@ describe('Test TableQueries Component', () => {
}); });
it('Error placeholder should display if there is no data', async () => { it('Error placeholder should display if there is no data', async () => {
(searchQuery as jest.Mock).mockImplementationOnce(() => (getQueriesList as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ Promise.resolve({
hits: { hits: [], total: { value: 0 } }, data: [],
paging: { total: 0 },
}) })
); );
render(<TableQueries {...mockTableQueriesProp} />, { render(<TableQueries {...mockTableQueriesProp} />, {
@ -105,9 +114,10 @@ describe('Test TableQueries Component', () => {
}); });
it('If paging count is more than 10, pagination should be visible', async () => { it('If paging count is more than 10, pagination should be visible', async () => {
(searchQuery as jest.Mock).mockImplementationOnce(() => (getQueriesList as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ Promise.resolve({
hits: { hits: [], total: { value: 11 } }, data: MOCK_QUERIES,
paging: { total: 11 },
}) })
); );
render(<TableQueries {...mockTableQueriesProp} />, { render(<TableQueries {...mockTableQueriesProp} />, {

View File

@ -38,10 +38,20 @@ import Qs from 'qs';
import React, { FC, useEffect, useMemo, useState } from 'react'; import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { getQueryById, patchQueries, updateQueryVote } from 'rest/queryAPI'; import {
getQueriesList,
getQueryById,
ListQueriesParams,
patchQueries,
updateQueryVote,
} from 'rest/queryAPI';
import { searchQuery } from 'rest/searchAPI'; import { searchQuery } from 'rest/searchAPI';
import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils'; import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
import { createQueryFilter, parseSearchParams } from 'utils/Query/QueryUtils'; import {
createQueryFilter,
parseSearchParams,
stringifySearchParams,
} from 'utils/Query/QueryUtils';
import { showErrorToast } from '../../utils/ToastUtils'; import { showErrorToast } from '../../utils/ToastUtils';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
@ -65,10 +75,13 @@ const TableQueries: FC<TableQueriesProp> = ({
const { searchParams, selectedFilters } = useMemo(() => { const { searchParams, selectedFilters } = useMemo(() => {
const searchData = parseSearchParams(location.search); const searchData = parseSearchParams(location.search);
const selectedFilters = { const selectedFilters =
user: searchData.user || [], searchData.user || searchData.team
team: searchData.team || [], ? {
}; user: searchData.user || [],
team: searchData.team || [],
}
: undefined;
return { return {
searchParams: searchData, searchParams: searchData,
@ -87,13 +100,19 @@ const TableQueries: FC<TableQueriesProp> = ({
DEFAULT_ENTITY_PERMISSION DEFAULT_ENTITY_PERMISSION
); );
const [currentPage, setCurrentPage] = useState( const [currentPage, setCurrentPage] = useState(
Number(searchParams.page) || INITIAL_PAGING_VALUE Number(searchParams.queryFrom) || INITIAL_PAGING_VALUE
); );
const [appliedFilter, setAppliedFilter] = const [appliedFilter, setAppliedFilter] = useState<
useState<QueryFiltersType>(selectedFilters); QueryFiltersType | undefined
>(selectedFilters);
const { getEntityPermission } = usePermissionProvider(); const { getEntityPermission } = usePermissionProvider();
const isNumberBasedPaging = useMemo(
() => Boolean(appliedFilter?.team.length || appliedFilter?.user.length),
[appliedFilter]
);
const fetchResourcePermission = async () => { const fetchResourcePermission = async () => {
if (isUndefined(selectedQuery)) { if (isUndefined(selectedQuery)) {
return; return;
@ -165,6 +184,45 @@ const TableQueries: FC<TableQueriesProp> = ({
showErrorToast(error as AxiosError); showErrorToast(error as AxiosError);
} }
}; };
const fetchTableQuery = async (
params?: ListQueriesParams,
activePage?: number
) => {
setIsLoading((pre) => ({ ...pre, query: true }));
try {
const queries = await getQueriesList({
...params,
limit: PAGE_SIZE,
entityId: tableId,
fields: 'owner,votes,tags,queryUsedIn',
});
if (queries.data.length === 0) {
setIsError((pre) => ({ ...pre, page: true }));
} else {
setTableQueries(queries);
const selectedQueryData = searchParams.query
? queries.data.find((query) => query.id === searchParams.query) ||
queries.data[0]
: queries.data[0];
setSelectedQuery(selectedQueryData);
history.push({
search: stringifySearchParams({
tableId,
after: params?.after,
query: selectedQueryData.id,
queryFrom: activePage,
}),
});
}
} catch (error) {
showErrorToast(error as AxiosError);
setIsError((pre) => ({ ...pre, page: true }));
} finally {
setIsLoading((pre) => ({ ...pre, query: false }));
}
};
const fetchFilterData = async (value?: QueryFiltersType, page?: number) => { const fetchFilterData = async (value?: QueryFiltersType, page?: number) => {
setIsLoading((pre) => ({ ...pre, query: true })); setIsLoading((pre) => ({ ...pre, query: true }));
const allFilter = flatMap(value); const allFilter = flatMap(value);
@ -196,13 +254,13 @@ const TableQueries: FC<TableQueriesProp> = ({
: queries[0]; : queries[0];
setSelectedQuery(selectedQueryData); setSelectedQuery(selectedQueryData);
history.push({ history.push({
search: Qs.stringify({ search: stringifySearchParams({
...searchParams,
...value, ...value,
page: pageNumber,
tableId, tableId,
query: selectedQueryData.id, query: selectedQueryData.id,
queryFrom: pageNumber,
}), }),
}); });
if (queries.length === 0) { if (queries.length === 0) {
@ -215,10 +273,14 @@ const TableQueries: FC<TableQueriesProp> = ({
} }
}; };
const pagingHandler = (cursorType: string | number) => { const pagingHandler = (cursorType: string | number, activePage?: number) => {
const { paging } = tableQueries;
if (isNumber(cursorType)) { if (isNumber(cursorType)) {
setCurrentPage(cursorType); setCurrentPage(cursorType);
fetchFilterData(appliedFilter, cursorType); fetchFilterData(appliedFilter, cursorType);
} else {
fetchTableQuery({ [cursorType]: paging[cursorType] }, activePage);
activePage && setCurrentPage(activePage);
} }
}; };
@ -238,20 +300,27 @@ const TableQueries: FC<TableQueriesProp> = ({
useEffect(() => { useEffect(() => {
setIsLoading((pre) => ({ ...pre, page: true })); setIsLoading((pre) => ({ ...pre, page: true }));
if (tableId && !isTableDeleted) { if (tableId && !isTableDeleted) {
fetchFilterData(selectedFilters, Number(searchParams.page)).finally( const initialFetch = selectedFilters
() => { ? fetchFilterData(selectedFilters, Number(searchParams.queryFrom))
setIsLoading((pre) => ({ ...pre, page: false })); : fetchTableQuery({ after: searchParams?.after });
} initialFetch.finally(() => {
); setIsLoading((pre) => ({ ...pre, page: false }));
});
} else { } else {
setIsLoading((pre) => ({ ...pre, page: false })); setIsLoading((pre) => ({ ...pre, page: false }));
} }
}, [tableId]); }, [tableId]);
const onOwnerFilterChange = (value: QueryFiltersType) => { const onOwnerFilterChange = (value: QueryFiltersType) => {
const { team, user } = value;
setIsError((pre) => ({ ...pre, search: false })); setIsError((pre) => ({ ...pre, search: false }));
setAppliedFilter(value); setAppliedFilter(value);
fetchFilterData(value, INITIAL_PAGING_VALUE); if (team.length || user.length) {
fetchFilterData(value, INITIAL_PAGING_VALUE);
} else {
fetchTableQuery();
}
}; };
if (isLoading.page) { if (isLoading.page) {
@ -265,6 +334,30 @@ const TableQueries: FC<TableQueriesProp> = ({
); );
} }
const queryTabBody = isError.search ? (
<Col className="flex-center font-medium" data-testid="no-queries" span={24}>
<ErrorPlaceHolder
heading={t('label.query-lowercase-plural')}
type={ERROR_PLACEHOLDER_TYPE.VIEW}
/>
</Col>
) : (
tableQueries.data.map((query) => (
<Col key={query.id} span={24}>
<QueryCard
isExpanded={false}
permission={queryPermissions}
query={query}
selectedId={selectedQuery?.id}
tableId={tableId}
onQuerySelection={handleSelectedQuery}
onQueryUpdate={handleQueryUpdate}
onUpdateVote={updateVote}
/>
</Col>
))
);
return ( return (
<Row className="h-full" id="tablequeries"> <Row className="h-full" id="tablequeries">
<Col span={18}> <Col span={18}>
@ -276,39 +369,13 @@ const TableQueries: FC<TableQueriesProp> = ({
<QueryFilters onFilterChange={onOwnerFilterChange} /> <QueryFilters onFilterChange={onOwnerFilterChange} />
</Col> </Col>
{isLoading.query ? ( {isLoading.query ? <Loader /> : queryTabBody}
<Loader />
) : isError.search ? (
<Col
className="flex-center font-medium"
data-testid="no-queries"
span={24}>
<ErrorPlaceHolder
heading={t('label.query-lowercase-plural')}
type={ERROR_PLACEHOLDER_TYPE.VIEW}
/>
</Col>
) : (
tableQueries.data.map((query) => (
<Col key={query.id} span={24}>
<QueryCard
isExpanded={false}
permission={queryPermissions}
query={query}
selectedId={selectedQuery?.id}
tableId={tableId}
onQuerySelection={handleSelectedQuery}
onQueryUpdate={handleQueryUpdate}
onUpdateVote={updateVote}
/>
</Col>
))
)}
<Col span={24}> <Col span={24}>
{tableQueries.paging.total > PAGE_SIZE && ( {tableQueries.paging.total > PAGE_SIZE && (
<NextPrevious <NextPrevious
isNumberBased
currentPage={currentPage} currentPage={currentPage}
isNumberBased={isNumberBasedPaging}
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
paging={tableQueries.paging} paging={tableQueries.paging}
pagingHandler={pagingHandler} pagingHandler={pagingHandler}

View File

@ -13,7 +13,7 @@
export const QUERY_USED_BY_TABLE_VIEW_CAP = 3; export const QUERY_USED_BY_TABLE_VIEW_CAP = 3;
export const QUERY_LINE_HEIGHT = 4; export const QUERY_LINE_HEIGHT = 4;
export const QUERY_DATE_FORMAT = "'On' MMMM do 'at' h:mma 'UTC'ZZ"; // eg: On March 6th at 6:20pm UTC+1 export const QUERY_DATE_FORMAT = "'On' MMMM dd 'at' h:mma 'UTC'ZZ"; // eg: On March 6th at 6:20pm UTC+1
export const QUERY_PAGE_LOADING_STATE = { export const QUERY_PAGE_LOADING_STATE = {
page: true, page: true,

View File

@ -48,3 +48,6 @@ export const parseSearchParams = (param: string) => {
// need to typecast into QuerySearchParams as Qs.parse returns as "Qs.ParsedQs" Type object // need to typecast into QuerySearchParams as Qs.parse returns as "Qs.ParsedQs" Type object
) as unknown as QuerySearchParams; ) as unknown as QuerySearchParams;
}; };
export const stringifySearchParams = (param: QuerySearchParams) => {
return Qs.stringify(param);
};