diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.interface.ts
new file mode 100644
index 00000000000..9736c5d859f
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.interface.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 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 interface DropDownOption {
+ key: string;
+ label: string;
+}
+
+export interface SearchDropdownProps {
+ label: string;
+ options: DropDownOption[];
+ searchKey: string;
+ selectedKeys: string[];
+ showClear?: boolean;
+ onChange: (values: string[], searchKey: string) => void;
+ onSearch: (searchText: string, searchKey: string) => void;
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.less b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.less
new file mode 100644
index 00000000000..5319c5c8fa0
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.less
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 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.
+ */
+
+.custom-dropdown-render {
+ // this is taken from antd dropdown menu box shadow
+ box-shadow: 0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%),
+ 0 9px 28px 8px rgb(0 0 0 / 5%);
+
+ .ant-dropdown-menu {
+ box-shadow: none;
+ padding: 0px;
+ .ant-dropdown-menu-item {
+ padding: 4px 0px;
+ }
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.test.tsx
new file mode 100644
index 00000000000..9735e319369
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.test.tsx
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2022 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 { act, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import SearchDropdown from './SearchDropdown';
+import { SearchDropdownProps } from './SearchDropdown.interface';
+
+const mockOnChange = jest.fn();
+const mockOnSearch = jest.fn();
+
+const searchOptions = [
+ { key: 'user.1', label: 'User 1' },
+ { key: 'user.2', label: 'User 2' },
+ { key: 'user.3', label: 'User 3' },
+ { key: 'user.4', label: 'User 4' },
+ { key: 'user.5', label: 'User 5' },
+];
+
+const mockProps: SearchDropdownProps = {
+ label: 'Owner',
+ options: searchOptions,
+ searchKey: 'owner.name',
+ selectedKeys: ['user.1'],
+ showClear: true,
+ onChange: mockOnChange,
+ onSearch: mockOnSearch,
+};
+
+describe('Search DropDown Component', () => {
+ it('Should render Dropdown components', async () => {
+ render();
+
+ const container = await screen.findByTestId('search-dropdown');
+
+ expect(container).toBeInTheDocument();
+
+ await act(async () => {
+ userEvent.click(container);
+ });
+
+ expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
+
+ expect((await screen.findByTestId('User 1')).textContent).toContain(
+ 'User 1'
+ );
+ expect((await screen.findByTestId('User 2')).textContent).toContain(
+ 'User 2'
+ );
+ expect((await screen.findByTestId('User 3')).textContent).toContain(
+ 'User 3'
+ );
+ expect((await screen.findByTestId('User 4')).textContent).toContain(
+ 'User 4'
+ );
+ expect((await screen.findByTestId('User 5')).textContent).toContain(
+ 'User 5'
+ );
+
+ const searchInput = await screen.findByTestId('search-input');
+
+ expect(searchInput).toBeInTheDocument();
+
+ const clearButton = await screen.findByTestId('clear-button');
+
+ expect(clearButton).toBeInTheDocument();
+ });
+
+ it('Selected keys option should be checked', async () => {
+ render();
+
+ const container = await screen.findByTestId('search-dropdown');
+
+ expect(container).toBeInTheDocument();
+
+ await act(async () => {
+ userEvent.click(container);
+ });
+
+ expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
+
+ // user.1 is selected key so should be checked
+ expect(await screen.findByTestId('user.1')).toBeChecked();
+ });
+
+ it('UnSelected keys option should not be checked', async () => {
+ render();
+
+ const container = await screen.findByTestId('search-dropdown');
+
+ expect(container).toBeInTheDocument();
+
+ await act(async () => {
+ userEvent.click(container);
+ });
+
+ expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
+
+ expect(await screen.findByTestId('user.2')).not.toBeChecked();
+ expect(await screen.findByTestId('user.3')).not.toBeChecked();
+ expect(await screen.findByTestId('user.4')).not.toBeChecked();
+ expect(await screen.findByTestId('user.5')).not.toBeChecked();
+ });
+
+ it('Should render the clear all button and click should work', async () => {
+ render();
+
+ const container = await screen.findByTestId('search-dropdown');
+
+ expect(container).toBeInTheDocument();
+
+ await act(async () => {
+ userEvent.click(container);
+ });
+
+ expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
+
+ const clearButton = await screen.findByTestId('clear-button');
+
+ expect(clearButton).toBeInTheDocument();
+
+ await act(async () => {
+ userEvent.click(clearButton);
+ });
+
+ expect(mockOnChange).toHaveBeenCalledWith([], 'owner.name');
+ });
+
+ it('Should not render the clear all button if showClear is false/undefined', async () => {
+ render();
+
+ const container = await screen.findByTestId('search-dropdown');
+
+ expect(container).toBeInTheDocument();
+
+ await act(async () => {
+ userEvent.click(container);
+ });
+
+ expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
+
+ const clearButton = screen.queryByTestId('clear-button');
+
+ expect(clearButton).not.toBeInTheDocument();
+ });
+
+ it('Search should work', async () => {
+ render();
+
+ const container = await screen.findByTestId('search-dropdown');
+
+ expect(container).toBeInTheDocument();
+
+ await act(async () => {
+ userEvent.click(container);
+ });
+
+ expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
+
+ const searchInput = await screen.findByTestId('search-input');
+
+ await act(async () => {
+ userEvent.type(searchInput, 'user');
+ });
+
+ expect(searchInput).toHaveValue('user');
+
+ expect(mockOnSearch).toHaveBeenCalledWith('user', 'owner.name');
+ });
+
+ it('On Change should work', async () => {
+ render();
+
+ const container = await screen.findByTestId('search-dropdown');
+
+ expect(container).toBeInTheDocument();
+
+ await act(async () => {
+ userEvent.click(container);
+ });
+
+ expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
+
+ const option2 = await screen.findByTestId('User 2');
+
+ await act(async () => {
+ userEvent.click(option2);
+ });
+
+ // onChange should be called with previous selected keys and current selected keys
+ expect(mockOnChange).toHaveBeenCalledWith(
+ ['user.1', 'user.2'],
+ 'owner.name'
+ );
+ });
+
+ it('Selected option should unselect on next click', async () => {
+ render();
+
+ const container = await screen.findByTestId('search-dropdown');
+
+ expect(container).toBeInTheDocument();
+
+ await act(async () => {
+ userEvent.click(container);
+ });
+
+ expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument();
+
+ const option1 = await screen.findByTestId('User 1');
+
+ await act(async () => {
+ userEvent.click(option1);
+ });
+
+ expect(mockOnChange).toHaveBeenCalledWith([], 'owner.name');
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx
new file mode 100644
index 00000000000..6ddcf63c847
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2022 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 { DownOutlined } from '@ant-design/icons';
+import {
+ Button,
+ Card,
+ Checkbox,
+ Dropdown,
+ Input,
+ MenuItemProps,
+ MenuProps,
+ Space,
+} from 'antd';
+import React, { ChangeEvent, FC, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { SearchDropdownProps } from './SearchDropdown.interface';
+import './SearchDropdown.less';
+
+const SearchDropdown: FC = ({
+ label,
+ options,
+ searchKey,
+ selectedKeys,
+ showClear,
+ onChange,
+ onSearch,
+}) => {
+ const { t } = useTranslation();
+
+ const [isDropDownOpen, setIsDropDownOpen] = useState(false);
+
+ // derive menu props from options and selected keys
+ const menuOptions: MenuProps['items'] = useMemo(() => {
+ return options.map((option) => {
+ const isSelected = selectedKeys.includes(option.key);
+
+ return {
+ key: option.key,
+ label: (
+
+
+ {option.label}
+
+ ),
+ };
+ });
+ }, [options, selectedKeys]);
+
+ // handle menu item click
+ const handleMenuItemClick: MenuItemProps['onClick'] = (info) => {
+ const currentKey = info.key;
+ const isSelected = selectedKeys.includes(currentKey);
+
+ const updatedValues = isSelected
+ ? selectedKeys.filter((v) => v !== currentKey)
+ : [...selectedKeys, currentKey];
+
+ // call on change with updated value
+ onChange(updatedValues, searchKey);
+ };
+
+ // handle clear all
+ const handleClear = () => onChange([], searchKey);
+
+ // handle search
+ const handleSearch = (e: ChangeEvent) => {
+ const { value } = e.target;
+
+ onSearch(value, searchKey);
+ };
+
+ return (
+ {
+ return (
+
+
+
+ {showClear && (
+
+ )}
+ {menuNode}
+
+
+ );
+ }}
+ key={searchKey}
+ menu={{ items: menuOptions, onClick: handleMenuItemClick }}
+ trigger={['click']}
+ visible={isDropDownOpen}
+ onVisibleChange={(visible) => setIsDropDownOpen(visible)}>
+
+
+ );
+};
+
+export default SearchDropdown;