mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-01 05:03:10 +00:00
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:
parent
445ac90ef9
commit
d79974dba1
@ -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
|
||||||
|
@ -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) => {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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} />, {
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user