[Feature] : Advance Search (#5427)

Co-authored-by: Vivek Ratnavel Subramanian <vivekratnavel90@gmail.com>
This commit is contained in:
Sachin Chaurasiya 2022-06-13 22:43:22 +05:30 committed by GitHub
parent 5537db2ab4
commit f5dbae14da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1118 additions and 74 deletions

View File

@ -42,7 +42,7 @@ export const SEARCH_ENTITY_TOPIC = {
export const SEARCH_ENTITY_DASHBOARD = { export const SEARCH_ENTITY_DASHBOARD = {
dashboard_1: { dashboard_1: {
term: 'Sales Dashboard', term: 'Slack Dashboard',
entity: MYDATA_SUMMARY_OPTIONS.dashboards, entity: MYDATA_SUMMARY_OPTIONS.dashboards,
}, },
dashboard_2: { dashboard_2: {
@ -52,9 +52,9 @@ export const SEARCH_ENTITY_DASHBOARD = {
}; };
export const SEARCH_ENTITY_PIPELINE = { export const SEARCH_ENTITY_PIPELINE = {
pipeline_1: { term: 'Hive ETL', entity: MYDATA_SUMMARY_OPTIONS.pipelines }, pipeline_1: { term: 'Snowflake ETL', entity: MYDATA_SUMMARY_OPTIONS.pipelines },
pipeline_2: { pipeline_2: {
term: 'Snowflake ETL', term: 'Hive ETL',
entity: MYDATA_SUMMARY_OPTIONS.pipelines, entity: MYDATA_SUMMARY_OPTIONS.pipelines,
}, },
pipeline_3: { pipeline_3: {

View File

@ -32,7 +32,7 @@ describe('Entity Details Page', () => {
// click on the 1st result and go to manage tab in entity details page // click on the 1st result and go to manage tab in entity details page
cy.wait(500); cy.wait(500);
cy.get('[data-testid="table-link"]').first().should('be.visible').click(); cy.get('[data-testid="table-link"]').first().should('be.visible').click();
cy.get('[data-testid="Manage"]').scrollIntoView().click(); cy.get('[data-testid="Manage"]').click();
// check for delete section and delete button is available or not // check for delete section and delete button is available or not
cy.get('[data-testid="danger-zone"]').scrollIntoView().should('be.visible'); cy.get('[data-testid="danger-zone"]').scrollIntoView().should('be.visible');

View File

@ -132,9 +132,12 @@ export const getSuggestedTeams = (term: string): Promise<AxiosResponse> => {
export const getUserSuggestions: Function = ( export const getUserSuggestions: Function = (
term: string term: string
): Promise<AxiosResponse> => { ): Promise<AxiosResponse> => {
return APIClient.get( const params = {
`/search/suggest?q=${term}&index=${SearchIndex.USER},${SearchIndex.TEAM}` q: term,
); index: `${SearchIndex.USER},${SearchIndex.TEAM}`,
};
return APIClient.get(`/search/suggest`, { params });
}; };
export const getSearchedUsers = ( export const getSearchedUsers = (
@ -193,3 +196,13 @@ export const deleteEntity: Function = (
return APIClient.delete(path); return APIClient.delete(path);
}; };
export const getAdvancedFieldOptions = (
q: string,
index: string,
field: string | undefined
): Promise<AxiosResponse> => {
const params = { index, field, q };
return APIClient.get(`/search/suggest`, { params });
};

View File

@ -184,6 +184,10 @@ const ActivityThreadPanel: FC<ActivityThreadPanelProp> = ({
fetchMoreThread(isInView as boolean, paging, isThreadLoading); fetchMoreThread(isInView as boolean, paging, isThreadLoading);
}, [paging, isThreadLoading, isInView]); }, [paging, isThreadLoading, isInView]);
useEffect(() => {
document.body.style.overflow = 'hidden';
}, []);
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div className={classNames('tw-h-full', className)}> <div className={classNames('tw-h-full', className)}>
<FeedPanelOverlay <FeedPanelOverlay

View File

@ -0,0 +1,164 @@
/*
* 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.
*/
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { AdvanceField } from '../Explore/explore.interface';
import AdvancedField from './AdvancedField';
const mockData = {
'metadata-suggest': [
{
text: 'clou',
offset: 0,
length: 4,
options: [
{
text: 'Cloud_Infra',
_index: 'team_search_index',
_type: '_doc',
_id: '267a4dd4-df64-400d-b4d1-3925d6f23885',
_score: 10,
_source: {
suggest: [
{
input: 'Cloud_Infra',
weight: 5,
},
{
input: 'Cloud_Infra',
weight: 10,
},
],
deleted: false,
// eslint-disable-next-line @typescript-eslint/camelcase
team_id: '267a4dd4-df64-400d-b4d1-3925d6f23885',
name: 'Cloud_Infra',
// eslint-disable-next-line @typescript-eslint/camelcase
display_name: 'Cloud_Infra',
// eslint-disable-next-line @typescript-eslint/camelcase
entity_type: 'team',
users: [],
owns: [],
// eslint-disable-next-line @typescript-eslint/camelcase
default_roles: [],
// eslint-disable-next-line @typescript-eslint/camelcase
last_updated_timestamp: 1654838173854,
},
},
],
},
],
};
jest.mock('../../axiosAPIs/miscAPI', () => ({
getAdvancedFieldOptions: jest
.fn()
.mockImplementation(() => Promise.resolve()),
getUserSuggestions: jest
.fn()
.mockImplementation(() => Promise.resolve({ data: { suggest: mockData } })),
}));
const index = 'table_search_index';
const field = { key: 'owner.name', value: undefined } as AdvanceField;
const onFieldRemove = jest.fn();
const onFieldValueSelect = jest.fn();
const mockProps = {
index,
field,
onFieldRemove,
onFieldValueSelect,
};
describe('Test AdvancedField Component', () => {
it('Should render advancedfield component', async () => {
const { findByTestId } = render(<AdvancedField {...mockProps} />);
const label = await findByTestId('field-label');
expect(label).toBeInTheDocument();
expect(label).toHaveTextContent('Owner:');
const searchSelect = await findByTestId('field-select');
expect(searchSelect).toBeInTheDocument();
const removeButton = await findByTestId('field-remove-button');
expect(removeButton).toBeInTheDocument();
});
it('Should call remove method on click of remove button', async () => {
const { findByTestId } = render(<AdvancedField {...mockProps} />);
const label = await findByTestId('field-label');
expect(label).toBeInTheDocument();
expect(label).toHaveTextContent('Owner:');
const searchSelect = await findByTestId('field-select');
expect(searchSelect).toBeInTheDocument();
const removeButton = await findByTestId('field-remove-button');
expect(removeButton).toBeInTheDocument();
fireEvent.click(removeButton);
expect(onFieldRemove).toHaveBeenCalledWith(field.key);
});
it('Should call select method on click of option', async () => {
const { findByTestId, findByRole, findAllByTestId } = render(
<AdvancedField {...mockProps} />
);
const label = await findByTestId('field-label');
expect(label).toBeInTheDocument();
expect(label).toHaveTextContent('Owner:');
const searchSelect = await findByTestId('field-select');
expect(searchSelect).toBeInTheDocument();
const removeButton = await findByTestId('field-remove-button');
expect(removeButton).toBeInTheDocument();
const searchInput = await findByRole('combobox');
expect(searchInput).toBeInTheDocument();
fireEvent.change(searchInput, { target: { value: 'cloud' } });
const fieldOptions = await findAllByTestId('field-option');
expect(fieldOptions).toHaveLength(
mockData['metadata-suggest'][0].options.length
);
fireEvent.click(fieldOptions[0]);
expect(onFieldValueSelect).toHaveBeenCalledWith({
key: 'owner.name',
value: 'Cloud_Infra',
});
});
});

View File

@ -0,0 +1,183 @@
/*
* 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.
*/
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Select } from 'antd';
import { AxiosError, AxiosResponse } from 'axios';
import { startCase } from 'lodash';
import React, { FC, useState } from 'react';
import {
getAdvancedFieldOptions,
getUserSuggestions,
} from '../../axiosAPIs/miscAPI';
import { MISC_FIELDS } from '../../constants/advanceSearch.constants';
import {
getAdvancedField,
getItemLabel,
} from '../../utils/AdvancedSearchUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { AdvanceField } from '../Explore/explore.interface';
interface Props {
index: string;
field: AdvanceField;
onFieldRemove: (value: string) => void;
onFieldValueSelect: (field: AdvanceField) => void;
}
interface Option {
label: string;
value: string;
}
interface InputProps {
options: Option[];
value: string | undefined;
handleChange: (value: string) => void;
handleSearch: (value: string) => void;
handleSelect: (value: string) => void;
handleClear: () => void;
}
const SearchInput = ({
options,
value,
handleChange,
handleSearch,
handleSelect,
handleClear,
}: InputProps) => {
const { Option } = Select;
const optionsElement = options.map((d) => (
<Option data-testid="field-option" key={d.value}>
{d.label}
</Option>
));
return (
<Select
allowClear
showSearch
bordered={false}
className="ant-advaced-field-select"
data-testid="field-select"
defaultActiveFirstOption={false}
dropdownClassName="ant-suggestion-dropdown"
filterOption={false}
notFoundContent={null}
placeholder="Search to Select"
showArrow={false}
value={value}
onChange={handleChange}
onClear={handleClear}
onSearch={handleSearch}
onSelect={handleSelect}>
{optionsElement}
</Select>
);
};
const AdvancedField: FC<Props> = ({
field,
onFieldRemove,
index,
onFieldValueSelect,
}) => {
const advancedField = getAdvancedField(field.key);
const [options, setOptions] = useState<Option[]>([]);
const [value, setValue] = useState<string | undefined>(field.value);
const fetchOptions = (query: string) => {
if (!MISC_FIELDS.includes(field.key)) {
getAdvancedFieldOptions(query, index, advancedField)
.then((res: AxiosResponse) => {
const suggestOptions =
res.data.suggest['metadata-suggest'][0].options ?? [];
const uniqueOptions = [
// eslint-disable-next-line
...new Set(suggestOptions.map((op: any) => op.text)),
];
setOptions(
uniqueOptions.map((op: unknown) => ({
label: op as string,
value: op as string,
}))
);
})
.catch((err: AxiosError) => showErrorToast(err));
} else {
getUserSuggestions(query)
.then((res: AxiosResponse) => {
const suggestOptions =
res.data.suggest['metadata-suggest'][0].options ?? [];
const uniqueOptions = [
// eslint-disable-next-line
...new Set(suggestOptions.map((op: any) => op._source.name)),
];
setOptions(
uniqueOptions.map((op: unknown) => ({
label: op as string,
value: op as string,
}))
);
})
.catch((err: AxiosError) => showErrorToast(err));
}
};
const handleSearch = (newValue: string) => {
if (newValue) {
fetchOptions(newValue);
} else {
setOptions([]);
}
};
const handleChange = (newValue: string) => {
setValue(newValue);
};
const handleOnSelect = (newValue: string) => {
onFieldValueSelect({ ...field, value: newValue });
};
const handleOnClear = () => {
onFieldValueSelect({ ...field, value: undefined });
};
return (
<div className="tw-bg-white tw-border tw-border-main tw-rounded tw-p-1 tw-px-2 tw-flex tw-justify-between">
<span className="tw-self-center" data-testid="field-label">
{startCase(getItemLabel(field.key))}:
</span>
<SearchInput
handleChange={handleChange}
handleClear={handleOnClear}
handleSearch={handleSearch}
handleSelect={handleOnSelect}
options={options}
value={value}
/>
<span
className="tw-cursor-pointer tw-self-center"
data-testid="field-remove-button"
onClick={() => onFieldRemove(field.key)}>
<FontAwesomeIcon className="tw-text-primary" icon="times" />
</span>
</div>
);
};
export default AdvancedField;

View File

@ -0,0 +1,73 @@
/*
* 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.
*/
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { AdvanceField } from '../Explore/explore.interface';
import AdvancedFields from './AdvancedFields';
jest.mock('./AdvancedField', () =>
jest
.fn()
.mockReturnValue(<div data-testid="advanced-field">AdvancedField</div>)
);
const index = 'table_search_index';
const fields = [
{ key: 'owner.name', value: undefined },
{ key: 'column_names', value: undefined },
] as AdvanceField[];
const onFieldRemove = jest.fn();
const onClear = jest.fn();
const onFieldValueSelect = jest.fn();
const mockProps = {
index,
fields,
onFieldRemove,
onClear,
onFieldValueSelect,
};
describe('Test AdvancedFields component', () => {
it('Should render AdvancedFields component', async () => {
const { findByTestId, findAllByTestId } = render(
<AdvancedFields {...mockProps} />
);
const fields = await findAllByTestId('advanced-field');
const clearButton = await findByTestId('clear-all-button');
expect(fields).toHaveLength(fields.length);
expect(clearButton).toBeInTheDocument();
});
it('Should call onClear method on click of Clear All button', async () => {
const { findByTestId, findAllByTestId } = render(
<AdvancedFields {...mockProps} />
);
const fields = await findAllByTestId('advanced-field');
const clearButton = await findByTestId('clear-all-button');
expect(fields).toHaveLength(fields.length);
expect(clearButton).toBeInTheDocument();
fireEvent.click(clearButton);
expect(onClear).toBeCalled();
});
});

View File

@ -0,0 +1,55 @@
/*
* 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.
*/
import { uniqueId } from 'lodash';
import React, { FC } from 'react';
import { AdvanceField } from '../Explore/explore.interface';
import AdvancedField from './AdvancedField';
interface Props {
index: string;
fields: Array<AdvanceField>;
onFieldRemove: (value: string) => void;
onClear: () => void;
onFieldValueSelect: (field: AdvanceField) => void;
}
const AdvancedFields: FC<Props> = ({
fields,
onFieldRemove,
onClear,
index,
onFieldValueSelect,
}) => {
return (
<div className="tw-flex tw-gap-2 tw-mb-3">
{fields.map((field) => (
<AdvancedField
field={field}
index={index}
key={uniqueId()}
onFieldRemove={onFieldRemove}
onFieldValueSelect={onFieldValueSelect}
/>
))}
<span
className="tw-text-primary tw-self-center tw-cursor-pointer"
data-testid="clear-all-button"
onClick={onClear}>
Clear All
</span>
</div>
);
};
export default AdvancedFields;

View File

@ -0,0 +1,114 @@
/*
* 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.
*/
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import {
COMMON_DROPDOWN_ITEMS,
TABLE_DROPDOWN_ITEMS,
} from '../../constants/advanceSearch.constants';
import { AdvanceField } from '../Explore/explore.interface';
import AdvancedSearchDropDown from './AdvancedSearchDropDown';
const mockItems = [...COMMON_DROPDOWN_ITEMS, ...TABLE_DROPDOWN_ITEMS];
jest.mock('../../utils/AdvancedSearchUtils', () => ({
getDropDownItems: jest
.fn()
.mockReturnValue([...COMMON_DROPDOWN_ITEMS, ...TABLE_DROPDOWN_ITEMS]),
}));
const onSelect = jest.fn();
const selectedItems = [] as AdvanceField[];
const index = 'table_search_index';
const mockPorps = {
selectedItems,
index,
onSelect,
};
describe('Test AdvancedSearch DropDown Component', () => {
it('Should render dropdown component', async () => {
const { findByTestId, findAllByTestId } = render(
<AdvancedSearchDropDown {...mockPorps} />
);
const dropdownLabel = await findByTestId('dropdown-label');
expect(dropdownLabel).toBeInTheDocument();
fireEvent.click(dropdownLabel);
const dropdownMenu = await findByTestId('dropdown-menu');
expect(dropdownMenu).toBeInTheDocument();
const menuItems = await findAllByTestId('dropdown-menu-item');
expect(menuItems).toHaveLength(mockItems.length);
});
it('Should call onSelect method on onClick option', async () => {
const { findByTestId, findAllByTestId } = render(
<AdvancedSearchDropDown {...mockPorps} />
);
const dropdownLabel = await findByTestId('dropdown-label');
expect(dropdownLabel).toBeInTheDocument();
fireEvent.click(dropdownLabel);
const dropdownMenu = await findByTestId('dropdown-menu');
expect(dropdownMenu).toBeInTheDocument();
const menuItems = await findAllByTestId('dropdown-menu-item');
expect(menuItems).toHaveLength(mockItems.length);
fireEvent.click(menuItems[0]);
expect(onSelect).toHaveBeenCalledWith(mockItems[0].key);
});
it('Selected option should be disabled', async () => {
const { findByTestId, findAllByTestId } = render(
<AdvancedSearchDropDown
{...mockPorps}
selectedItems={[{ key: mockItems[0].key, value: undefined }]}
/>
);
const dropdownLabel = await findByTestId('dropdown-label');
expect(dropdownLabel).toBeInTheDocument();
fireEvent.click(dropdownLabel);
const dropdownMenu = await findByTestId('dropdown-menu');
expect(dropdownMenu).toBeInTheDocument();
const menuItems = await findAllByTestId('dropdown-menu-item');
expect(menuItems).toHaveLength(mockItems.length);
expect(menuItems[0]).toHaveAttribute('aria-disabled', 'true');
fireEvent.click(menuItems[0]);
expect(onSelect).not.toHaveBeenCalledWith(mockItems[0].key);
});
});

View File

@ -0,0 +1,55 @@
/*
* 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.
*/
import { Dropdown, Menu } from 'antd';
import React, { FC } from 'react';
import { getDropDownItems } from '../../utils/AdvancedSearchUtils';
import { normalLink } from '../../utils/styleconstant';
import { dropdownIcon as DropdownIcon } from '../../utils/svgconstant';
import { AdvanceField } from '../Explore/explore.interface';
interface Props {
index: string;
selectedItems: Array<AdvanceField>;
onSelect: (filter: string) => void;
}
const AdvancedSearchDropDown: FC<Props> = ({
index,
onSelect,
selectedItems,
}) => {
const items = getDropDownItems(index).map((item) => ({
...item,
onClick: () => onSelect(item.key),
disabled: selectedItems.some((i) => item.key === i.key),
'data-testid': 'dropdown-menu-item',
}));
const menu = <Menu data-testid="dropdown-menu" items={items} />;
return (
<Dropdown
className="tw-self-center tw-mr-2 tw-cursor-pointer"
data-testid="dropdown"
overlay={menu}
trigger={['click']}>
<div className="tw-text-primary" data-testid="dropdown-label">
<span className="tw-mr-2">Advanced Search</span>
<DropdownIcon style={{ color: normalLink, margin: '0px' }} />
</div>
</Dropdown>
);
};
export default AdvancedSearchDropDown;

View File

@ -36,7 +36,6 @@ import { useHistory, useLocation } from 'react-router-dom';
import { Button } from '../../components/buttons/Button/Button'; import { Button } from '../../components/buttons/Button/Button';
import ErrorPlaceHolderES from '../../components/common/error-with-placeholder/ErrorPlaceHolderES'; import ErrorPlaceHolderES from '../../components/common/error-with-placeholder/ErrorPlaceHolderES';
import FacetFilter from '../../components/common/facetfilter/FacetFilter'; import FacetFilter from '../../components/common/facetfilter/FacetFilter';
import DropDownList from '../../components/dropdown/DropDownList';
import SearchedData from '../../components/searched-data/SearchedData'; import SearchedData from '../../components/searched-data/SearchedData';
import { import {
getExplorePathWithSearch, getExplorePathWithSearch,
@ -66,9 +65,11 @@ import {
import { formatDataResponse } from '../../utils/APIUtils'; import { formatDataResponse } from '../../utils/APIUtils';
import { getCountBadge } from '../../utils/CommonUtils'; import { getCountBadge } from '../../utils/CommonUtils';
import { getFilterCount, getFilterString } from '../../utils/FilterUtils'; import { getFilterCount, getFilterString } from '../../utils/FilterUtils';
import { dropdownIcon as DropDownIcon } from '../../utils/svgconstant'; import AdvancedFields from '../AdvancedSearch/AdvancedFields';
import AdvancedSearchDropDown from '../AdvancedSearch/AdvancedSearchDropDown';
import PageLayout from '../containers/PageLayout'; import PageLayout from '../containers/PageLayout';
import { ExploreProps } from './explore.interface'; import { AdvanceField, ExploreProps } from './explore.interface';
import SortingDropDown from './SortingDropDown';
const Explore: React.FC<ExploreProps> = ({ const Explore: React.FC<ExploreProps> = ({
tabCounts, tabCounts,
@ -105,18 +106,16 @@ const Explore: React.FC<ExploreProps> = ({
...filterObject, ...filterObject,
...searchFilter, ...searchFilter,
}); });
const [currentPage, setCurrentPage] = useState<number>(1); const [currentPage, setCurrentPage] = useState<number>(1);
const [totalNumberOfValue, setTotalNumberOfValues] = useState<number>(0); const [totalNumberOfValue, setTotalNumberOfValues] = useState<number>(0);
const [aggregations, setAggregations] = useState<Array<AggregationType>>([]); const [aggregations, setAggregations] = useState<Array<AggregationType>>([]);
const [searchTag, setSearchTag] = useState<string>(location.search); const [searchTag, setSearchTag] = useState<string>(location.search);
const [fieldListVisible, setFieldListVisible] = useState<boolean>(false);
const [sortField, setSortField] = useState<string>(sortValue); const [sortField, setSortField] = useState<string>(sortValue);
const [sortOrder, setSortOrder] = useState<string>(INITIAL_SORT_ORDER); const [sortOrder, setSortOrder] = useState<string>(INITIAL_SORT_ORDER);
const [searchIndex, setSearchIndex] = useState<string>(getCurrentIndex(tab)); const [searchIndex, setSearchIndex] = useState<string>(getCurrentIndex(tab));
const [currentTab, setCurrentTab] = useState<number>(getCurrentTab(tab)); const [currentTab, setCurrentTab] = useState<number>(getCurrentTab(tab));
const [fieldList, setFieldList] =
useState<Array<{ name: string; value: string }>>(tableSortingFields);
const [isEntityLoading, setIsEntityLoading] = useState(true); const [isEntityLoading, setIsEntityLoading] = useState(true);
const [isFilterSet, setIsFilterSet] = useState<boolean>( const [isFilterSet, setIsFilterSet] = useState<boolean>(
!isEmpty(initialFilter) !isEmpty(initialFilter)
@ -125,6 +124,43 @@ const Explore: React.FC<ExploreProps> = ({
const isMounting = useRef(true); const isMounting = useRef(true);
const forceSetAgg = useRef(false); const forceSetAgg = useRef(false);
const previsouIndex = usePrevious(searchIndex); const previsouIndex = usePrevious(searchIndex);
const [fieldList, setFieldList] =
useState<Array<{ name: string; value: string }>>(tableSortingFields);
const [selectedAdvancedFields, setSelectedAdvancedField] = useState<
Array<AdvanceField>
>([]);
const onAdvancedFieldSelect = (value: string) => {
const flag = selectedAdvancedFields.some((field) => field.key === value);
if (!flag) {
setSelectedAdvancedField((pre) => [
...pre,
{ key: value, value: undefined },
]);
}
};
const onAdvancedFieldRemove = (value: string) => {
setSelectedAdvancedField((pre) =>
pre.filter((field) => field.key !== value)
);
};
const onAdvancedFieldClear = () => {
setSelectedAdvancedField([]);
};
const onAdvancedFieldValueSelect = (field: AdvanceField) => {
setSelectedAdvancedField((pre) => {
return pre.map((preField) => {
if (preField.key === field.key) {
return field;
} else {
return preField;
}
});
});
};
const handleSelectedFilter = ( const handleSelectedFilter = (
checked: boolean, checked: boolean,
@ -160,11 +196,16 @@ const Explore: React.FC<ExploreProps> = ({
handleFilterChange(filterData); handleFilterChange(filterData);
}; };
const handleFieldDropDown = (value: string) => {
setSortField(value);
};
const handleShowDeleted = (checked: boolean) => { const handleShowDeleted = (checked: boolean) => {
onShowDeleted(checked); onShowDeleted(checked);
}; };
const onClearFilterHandler = (type: string[], isForceClear = false) => { const onClearFilterHandler = (type: string[], isForceClear = false) => {
setSelectedAdvancedField([]);
const updatedFilter = type.reduce((filterObj, type) => { const updatedFilter = type.reduce((filterObj, type) => {
return { ...filterObj, [type]: [] }; return { ...filterObj, [type]: [] };
}, {}); }, {});
@ -335,44 +376,26 @@ const Explore: React.FC<ExploreProps> = ({
return facetFilters; return facetFilters;
}; };
const handleFieldDropDown = (
_e: React.MouseEvent<HTMLElement, MouseEvent>,
value?: string
) => {
setSortField(value || sortField);
setFieldListVisible(false);
};
const handleOrder = (value: string) => { const handleOrder = (value: string) => {
setSortOrder(value); setSortOrder(value);
}; };
const getSortingElements = () => { const getSortingElements = () => {
return ( return (
<div className="tw-flex tw-gap-2"> <div className="tw-flex">
<div className="tw-mt-4"> <AdvancedSearchDropDown
<span className="tw-mr-2">Sort by:</span> index={searchIndex}
<span className="tw-relative"> selectedItems={selectedAdvancedFields}
<Button onSelect={onAdvancedFieldSelect}
className="focus:tw-no-underline" />
data-testid="sortBy"
size="custom" <SortingDropDown
theme="primary" fieldList={fieldList}
variant="link" handleFieldDropDown={handleFieldDropDown}
onClick={() => setFieldListVisible((visible) => !visible)}> sortField={sortField}
{fieldList.find((field) => field.value === sortField)?.name || />
'Relevance'}
<DropDownIcon /> <div className="tw-flex">
</Button>
{fieldListVisible && (
<DropDownList
dropDownList={fieldList}
value={sortField}
onSelect={handleFieldDropDown}
/>
)}
</span>
</div>
<div className="tw-mt-2 tw-flex tw-gap-2">
{sortOrder === 'asc' ? ( {sortOrder === 'asc' ? (
<button onClick={() => handleOrder('desc')}> <button onClick={() => handleOrder('desc')}>
<FontAwesomeIcon <FontAwesomeIcon
@ -486,6 +509,17 @@ const Explore: React.FC<ExploreProps> = ({
} }
}; };
const handleAdvancedSearch = (advancedFields: AdvanceField[]) => {
const advancedFilterObject: FilterObject = {};
advancedFields.forEach((field) => {
if (field.value) {
advancedFilterObject[field.key] = [field.value];
}
});
handleFilterChange(advancedFilterObject);
};
useEffect(() => { useEffect(() => {
handleSearchText(searchQuery || emptyValue); handleSearchText(searchQuery || emptyValue);
setCurrentPage(1); setCurrentPage(1);
@ -493,11 +527,8 @@ const Explore: React.FC<ExploreProps> = ({
useEffect(() => { useEffect(() => {
setFieldList(tabsInfo[getCurrentTab(tab) - 1].sortingFields); setFieldList(tabsInfo[getCurrentTab(tab) - 1].sortingFields);
setSortField( // if search text is there then set sortfield as ''(Relevance)
searchQuery setSortField(searchText ? '' : tabsInfo[getCurrentTab(tab) - 1].sortField);
? tabsInfo[getCurrentTab(tab) - 1].sortField
: INITIAL_SORT_FIELD
);
setSortOrder(INITIAL_SORT_ORDER); setSortOrder(INITIAL_SORT_ORDER);
setCurrentTab(getCurrentTab(tab)); setCurrentTab(getCurrentTab(tab));
setSearchIndex(getCurrentIndex(tab)); setSearchIndex(getCurrentIndex(tab));
@ -525,7 +556,6 @@ const Explore: React.FC<ExploreProps> = ({
useEffect(() => { useEffect(() => {
forceSetAgg.current = true; forceSetAgg.current = true;
if (!isMounting.current) { if (!isMounting.current) {
resetFilters();
fetchTableData(); fetchTableData();
} }
}, [searchText, searchIndex, showDeleted]); }, [searchText, searchIndex, showDeleted]);
@ -593,6 +623,32 @@ const Explore: React.FC<ExploreProps> = ({
} }
}, [filters]); }, [filters]);
/**
* on index change clear the filters
*/
useEffect(() => {
setSelectedAdvancedField([]);
}, [searchIndex]);
/**
* if search query is there then make sortfield as empty (Relevance)
* otherwise change it to INITIAL_SORT_FIELD (last_updated)
*/
useEffect(() => {
if (searchText) {
setSortField('');
} else {
setSortField(INITIAL_SORT_FIELD);
}
}, [searchText]);
/**
* on advance field change call handleAdvancedSearch methdod
*/
useEffect(() => {
handleAdvancedSearch(selectedAdvancedFields);
}, [selectedAdvancedFields]);
// alwyas Keep this useEffect at the end... // alwyas Keep this useEffect at the end...
useEffect(() => { useEffect(() => {
isMounting.current = false; isMounting.current = false;
@ -614,12 +670,24 @@ const Explore: React.FC<ExploreProps> = ({
); );
}; };
const advanceFieldCheck =
!connectionError && Boolean(selectedAdvancedFields.length);
return ( return (
<Fragment> <Fragment>
{!connectionError && getTabs()} {!connectionError && getTabs()}
<PageLayout <PageLayout
leftPanel={Boolean(!error) && fetchLeftPanel()} leftPanel={Boolean(!error) && fetchLeftPanel()}
rightPanel={Boolean(!error) && <></>}> rightPanel={Boolean(!error) && <></>}>
{advanceFieldCheck && (
<AdvancedFields
fields={selectedAdvancedFields}
index={searchIndex}
onClear={onAdvancedFieldClear}
onFieldRemove={onAdvancedFieldRemove}
onFieldValueSelect={onAdvancedFieldValueSelect}
/>
)}
{error ? ( {error ? (
<ErrorPlaceHolderES errorMessage={error} type="error" /> <ErrorPlaceHolderES errorMessage={error} type="error" />
) : ( ) : (

View File

@ -0,0 +1,73 @@
/*
* 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.
*/
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { tableSortingFields } from '../../constants/constants';
import SortingDropDown from './SortingDropDown';
const handleFieldDropDown = jest.fn();
const fieldList = tableSortingFields;
const sortField = '';
const mockPorps = {
fieldList,
sortField,
handleFieldDropDown,
};
describe('Test Sorting DropDown Component', () => {
it('Should render dropdown component', async () => {
const { findByTestId, findAllByTestId } = render(
<SortingDropDown {...mockPorps} />
);
const dropdownLabel = await findByTestId('dropdown-label');
expect(dropdownLabel).toBeInTheDocument();
fireEvent.click(dropdownLabel);
const dropdownMenu = await findByTestId('dropdown-menu');
expect(dropdownMenu).toBeInTheDocument();
const menuItems = await findAllByTestId('dropdown-menu-item');
expect(menuItems).toHaveLength(fieldList.length);
});
it('Should call onSelect method on onClick option', async () => {
const { findByTestId, findAllByTestId } = render(
<SortingDropDown {...mockPorps} />
);
const dropdownLabel = await findByTestId('dropdown-label');
expect(dropdownLabel).toBeInTheDocument();
fireEvent.click(dropdownLabel);
const dropdownMenu = await findByTestId('dropdown-menu');
expect(dropdownMenu).toBeInTheDocument();
const menuItems = await findAllByTestId('dropdown-menu-item');
expect(menuItems).toHaveLength(fieldList.length);
fireEvent.click(menuItems[0]);
expect(handleFieldDropDown).toHaveBeenCalledWith('last_updated_timestamp');
});
});

View File

@ -0,0 +1,55 @@
/*
* 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.
*/
import { Dropdown, Menu } from 'antd';
import React, { FC } from 'react';
import { normalLink } from '../../utils/styleconstant';
import { dropdownIcon as DropDownIcon } from '../../utils/svgconstant';
interface Props {
sortField: string;
fieldList: Array<{ name: string; value: string }>;
handleFieldDropDown: (value: string) => void;
}
const SortingDropDown: FC<Props> = ({
fieldList,
handleFieldDropDown,
sortField,
}) => {
const items = fieldList.map((field) => ({
label: field.name,
key: field.value,
onClick: () => handleFieldDropDown(field.value),
'data-testid': 'dropdown-menu-item',
}));
const menu = <Menu data-testid="dropdown-menu" items={items} />;
const label = fieldList.find((field) => field.value === sortField)?.name;
return (
<Dropdown
className="tw-self-center tw-mr-2 tw-cursor-pointer"
data-testid="dropdown"
overlay={menu}
trigger={['click']}>
<div className="tw-text-primary" data-testid="dropdown-label">
<span className="tw-mr-2">{label}</span>
<DropDownIcon style={{ color: normalLink, margin: '0px' }} />
</div>
</Dropdown>
);
};
export default SortingDropDown;

View File

@ -60,3 +60,8 @@ export interface ExploreProps {
fetchData: (value: SearchDataFunctionType[]) => void; fetchData: (value: SearchDataFunctionType[]) => void;
onShowDeleted: (checked: boolean) => void; onShowDeleted: (checked: boolean) => void;
} }
export interface AdvanceField {
key: string;
value: string | undefined;
}

View File

@ -229,15 +229,16 @@ const Appbar: React.FC = (): JSX.Element => {
const searchHandler = (value: string) => { const searchHandler = (value: string) => {
setIsOpen(false); setIsOpen(false);
addToRecentSearched(value); addToRecentSearched(value);
history.push( history.push({
getExplorePathWithSearch( pathname: getExplorePathWithSearch(
value, value,
// this is for if user is searching from another page // this is for if user is searching from another page
location.pathname.startsWith(ROUTES.EXPLORE) location.pathname.startsWith(ROUTES.EXPLORE)
? appState.explorePageTab ? appState.explorePageTab
: 'tables' : 'tables'
) ),
); search: location.search,
});
}; };
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {

View File

@ -0,0 +1,66 @@
/*
* 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.
*/
export const COMMON_DROPDOWN_ITEMS = [
{
label: 'Owner',
key: 'owner.name',
},
{
label: 'Tag',
key: 'tags',
},
{
label: 'Service',
key: 'servicename',
},
];
export const TABLE_DROPDOWN_ITEMS = [
{
label: 'Column',
key: 'column_names',
},
{
label: 'Schema',
key: 'databaseschema',
},
{
label: 'Database',
key: 'database',
},
];
export const DASHBOARD_DROPDOWN_ITEMS = [
{
label: 'Chart',
key: 'chart_names',
},
];
export const PIPELINE_DROPDOWN_ITEMS = [
{
label: 'Task',
key: 'task_names',
},
];
export const ALL_DROPDOWN_ITEMS = [
...COMMON_DROPDOWN_ITEMS,
...TABLE_DROPDOWN_ITEMS,
...DASHBOARD_DROPDOWN_ITEMS,
...PIPELINE_DROPDOWN_ITEMS,
];
export const MISC_FIELDS = ['owner.name', 'tags'];

View File

@ -116,15 +116,15 @@ export const tableSortingFields = [
value: 'last_updated_timestamp', value: 'last_updated_timestamp',
}, },
{ name: 'Weekly Usage', value: 'weekly_stats' }, { name: 'Weekly Usage', value: 'weekly_stats' },
// { name: 'Daily Usage', value: 'daily_stats' }, { name: 'Relevance', value: '' },
// { name: 'Monthly Usage', value: 'monthly_stats' },
]; ];
export const topicSortingFields = [ export const entitySortingFields = [
{ {
name: 'Last Updated', name: 'Last Updated',
value: 'last_updated_timestamp', value: 'last_updated_timestamp',
}, },
{ name: 'Relevance', value: '' },
]; ];
export const sortingOrder = [ export const sortingOrder = [

View File

@ -16,7 +16,7 @@ import { AggregationType, Bucket, FilterObject } from 'Models';
import { SearchIndex } from '../enums/search.enum'; import { SearchIndex } from '../enums/search.enum';
import { getFilterKey } from '../utils/FilterUtils'; import { getFilterKey } from '../utils/FilterUtils';
import { Icons } from '../utils/SvgUtils'; import { Icons } from '../utils/SvgUtils';
import { tableSortingFields, tiers, topicSortingFields } from './constants'; import { entitySortingFields, tableSortingFields, tiers } from './constants';
export const INITIAL_SORT_FIELD = 'last_updated_timestamp'; export const INITIAL_SORT_FIELD = 'last_updated_timestamp';
export const INITIAL_SORT_ORDER = 'desc'; export const INITIAL_SORT_ORDER = 'desc';
@ -232,7 +232,7 @@ export const tabsInfo = [
label: 'Tables', label: 'Tables',
index: SearchIndex.TABLE, index: SearchIndex.TABLE,
sortingFields: tableSortingFields, sortingFields: tableSortingFields,
sortField: '', sortField: INITIAL_SORT_FIELD,
tab: 1, tab: 1,
path: 'tables', path: 'tables',
icon: Icons.TABLE_GREY, icon: Icons.TABLE_GREY,
@ -241,8 +241,8 @@ export const tabsInfo = [
{ {
label: 'Topics', label: 'Topics',
index: SearchIndex.TOPIC, index: SearchIndex.TOPIC,
sortingFields: topicSortingFields, sortingFields: entitySortingFields,
sortField: '', sortField: INITIAL_SORT_FIELD,
tab: 2, tab: 2,
path: 'topics', path: 'topics',
icon: Icons.TOPIC_GREY, icon: Icons.TOPIC_GREY,
@ -251,8 +251,8 @@ export const tabsInfo = [
{ {
label: 'Dashboards', label: 'Dashboards',
index: SearchIndex.DASHBOARD, index: SearchIndex.DASHBOARD,
sortingFields: topicSortingFields, sortingFields: entitySortingFields,
sortField: '', sortField: INITIAL_SORT_FIELD,
tab: 3, tab: 3,
path: 'dashboards', path: 'dashboards',
icon: Icons.DASHBOARD_GREY, icon: Icons.DASHBOARD_GREY,
@ -261,8 +261,8 @@ export const tabsInfo = [
{ {
label: 'Pipelines', label: 'Pipelines',
index: SearchIndex.PIPELINE, index: SearchIndex.PIPELINE,
sortingFields: topicSortingFields, sortingFields: entitySortingFields,
sortField: '', sortField: INITIAL_SORT_FIELD,
tab: 4, tab: 4,
path: 'pipelines', path: 'pipelines',
icon: Icons.PIPELINE_GREY, icon: Icons.PIPELINE_GREY,
@ -271,8 +271,8 @@ export const tabsInfo = [
{ {
label: 'ML Models', label: 'ML Models',
index: SearchIndex.MLMODEL, index: SearchIndex.MLMODEL,
sortingFields: topicSortingFields, sortingFields: entitySortingFields,
sortField: '', sortField: INITIAL_SORT_FIELD,
tab: 5, tab: 5,
path: 'mlmodels', path: 'mlmodels',
icon: '', icon: '',

View File

@ -0,0 +1,21 @@
/*
* 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.
*/
export enum AdvancedFields {
COLUMN = 'column_suggest',
DATABASE = 'database_suggest',
SCHEMA = 'schema_suggest',
SERVICE = 'service_suggest',
CHART = 'chart_suggest',
TASK = 'task_suggest',
}

View File

@ -45,7 +45,6 @@ import {
getQueryParam, getQueryParam,
getSearchFilter, getSearchFilter,
INITIAL_FROM, INITIAL_FROM,
INITIAL_SORT_FIELD,
INITIAL_SORT_ORDER, INITIAL_SORT_ORDER,
tabsInfo, tabsInfo,
ZERO_SIZE, ZERO_SIZE,
@ -81,9 +80,7 @@ const ExplorePage: FunctionComponent = () => {
const [searchResult, setSearchResult] = useState<ExploreSearchData>(); const [searchResult, setSearchResult] = useState<ExploreSearchData>();
const [showDeleted, setShowDeleted] = useState(false); const [showDeleted, setShowDeleted] = useState(false);
const [initialSortField] = useState<string>( const [initialSortField] = useState<string>(
searchQuery tabsInfo[getCurrentTab(tab) - 1].sortField
? tabsInfo[getCurrentTab(tab) - 1].sortField
: INITIAL_SORT_FIELD
); );
const handleSearchText = (text: string) => { const handleSearchText = (text: string) => {

View File

@ -992,3 +992,22 @@ code {
.ant-card-extra { .ant-card-extra {
padding: 0px; padding: 0px;
} }
.ant-advaced-field-select {
color: #7147e8;
min-width: 130px;
}
.ant-suggestion-dropdown {
min-width: 200px !important;
}
.ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
background-color: #dbd1f9;
}
.ant-select-single:not(.ant-select-customize-input) .ant-select-selector {
padding: 0px 4px;
}
.ant-select-single .ant-select-selector .ant-select-selection-search {
left: 4px;
}

View File

@ -0,0 +1,78 @@
/*
* 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.
*/
import { isUndefined } from 'lodash';
import {
ALL_DROPDOWN_ITEMS,
COMMON_DROPDOWN_ITEMS,
DASHBOARD_DROPDOWN_ITEMS,
PIPELINE_DROPDOWN_ITEMS,
TABLE_DROPDOWN_ITEMS,
} from '../constants/advanceSearch.constants';
import { AdvancedFields } from '../enums/AdvancedSearch.enum';
import { SearchIndex } from '../enums/search.enum';
export const getDropDownItems = (index: string) => {
switch (index) {
case SearchIndex.TABLE:
return [...TABLE_DROPDOWN_ITEMS, ...COMMON_DROPDOWN_ITEMS];
case SearchIndex.TOPIC:
return [...COMMON_DROPDOWN_ITEMS];
case SearchIndex.DASHBOARD:
return [...DASHBOARD_DROPDOWN_ITEMS, ...COMMON_DROPDOWN_ITEMS];
case SearchIndex.PIPELINE:
return [...PIPELINE_DROPDOWN_ITEMS, ...COMMON_DROPDOWN_ITEMS];
case SearchIndex.MLMODEL:
return [
...COMMON_DROPDOWN_ITEMS.filter((item) => item.key !== 'service_type'),
];
default:
return [];
}
};
export const getItemLabel = (key: string) => {
const item = ALL_DROPDOWN_ITEMS.find((dItem) => dItem.key === key);
return !isUndefined(item) ? item.label : 'label';
};
export const getAdvancedField = (field: string) => {
switch (field) {
case 'column_names':
return AdvancedFields.COLUMN;
case 'databaseschema':
return AdvancedFields.SCHEMA;
case 'database':
return AdvancedFields.DATABASE;
case 'chart_names':
return AdvancedFields.CHART;
case 'task_names':
return AdvancedFields.TASK;
case 'servicename':
return AdvancedFields.SERVICE;
default:
return;
}
};

View File

@ -35,7 +35,7 @@ export const getSearchAPIQuery = (
filters ? ` AND ${filters}` : '' filters ? ` AND ${filters}` : ''
}&from=${start}&size=${size}${onlyDeleted ? '&deleted=true' : ''}${ }&from=${start}&size=${size}${onlyDeleted ? '&deleted=true' : ''}${
sortField ? `&sort_field=${sortField}` : '' sortField ? `&sort_field=${sortField}` : ''
}${sortOrder ? `&sort_order=${sortOrder}` : ''}${ }${sortOrder && sortField ? `&sort_order=${sortOrder}` : ''}${
searchIndex ? `&index=${searchIndex}` : '' searchIndex ? `&index=${searchIndex}` : ''
}${trackTotalHits ? '&track_total_hits=true' : ''}`; }${trackTotalHits ? '&track_total_hits=true' : ''}`;
}; };