From e102f0eb7c9318504b33a5a8518f317b671fa325 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Sat, 30 Jul 2022 02:42:58 +0530 Subject: [PATCH] feat(ui): support command + k global search support (#6322) --- .../integration/Pages/Glossary.spec.js | 12 +- .../src/main/resources/ui/src/App.tsx | 5 +- .../ui/src/assets/svg/command-button.svg | 4 + .../ui/src/assets/svg/control-button.svg | 4 + .../ui/src/assets/svg/empty-img-default.svg | 9 + .../resources/ui/src/assets/svg/k-button.svg | 4 + .../AddIngestion/Steps/ConfigureIngestion.tsx | 2 +- .../GlobalSearchProvider.tsx | 134 +++++++ .../GlobalSearchSuggestions.interface.ts | 63 ++++ .../GlobalSearchSuggestions.less | 19 + .../GlobalSearchSuggestions.tsx | 333 ++++++++++++++++++ .../ServiceConfig/ConnectionConfigForm.tsx | 2 +- .../common/CmdKIcon/CmdKIcon.component.tsx | 46 +++ .../common/FormBuilder/FormBuilder.tsx | 2 +- .../ui/src/components/nav-bar/NavBar.tsx | 35 +- .../resources/ui/src/utils/KeyboardUtil.ts | 19 + .../resources/ui/src/utils/NavigatorUtils.ts | 29 ++ 17 files changed, 700 insertions(+), 22 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/command-button.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/control-button.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/empty-img-default.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/k-button.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchProvider.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.interface.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.less create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/CmdKIcon/CmdKIcon.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/KeyboardUtil.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/NavigatorUtils.ts diff --git a/openmetadata-ui/src/main/resources/ui/cypress/integration/Pages/Glossary.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/integration/Pages/Glossary.spec.js index 42d7c821679..84d76a55bd8 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/integration/Pages/Glossary.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/integration/Pages/Glossary.spec.js @@ -97,8 +97,8 @@ describe('Glossary page should work properly', () => { cy.goToHomePage(); // redirecting to glossary page cy.get( - '.tw-ml-5 > [data-testid="dropdown-item"] > div > [data-testid="menu-button"]' - ) + '.tw-ml-5 > [data-testid="dropdown-item"] > div > [data-testid="menu-button"]' + ) .scrollIntoView() .should('be.visible') .click(); @@ -322,8 +322,8 @@ describe('Glossary page should work properly', () => { addNewTagToEntity(entity, term); cy.get( - '.tw-ml-5 > [data-testid="dropdown-item"] > div > [data-testid="menu-button"]' - ) + '.tw-ml-5 > [data-testid="dropdown-item"] > div > [data-testid="menu-button"]' + ) .scrollIntoView() .should('be.visible') .click(); @@ -378,8 +378,8 @@ describe('Glossary page should work properly', () => { .click(); cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click(); cy.get( - '.tw-ml-5 > [data-testid="dropdown-item"] > div > [data-testid="menu-button"]' - ) + '.tw-ml-5 > [data-testid="dropdown-item"] > div > [data-testid="menu-button"]' + ) .scrollIntoView() .should('be.visible') .click(); diff --git a/openmetadata-ui/src/main/resources/ui/src/App.tsx b/openmetadata-ui/src/main/resources/ui/src/App.tsx index 21eba39259e..10ca356b707 100644 --- a/openmetadata-ui/src/main/resources/ui/src/App.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/App.tsx @@ -29,6 +29,7 @@ import { BrowserRouter as Router } from 'react-router-dom'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.min.css'; import { AuthProvider } from './authentication/auth-provider/AuthProvider'; +import GlobalSearchProvider from './components/GlobalSearchProvider/GlobalSearchProvider'; import WebSocketProvider from './components/web-scoket/web-scoket.provider'; import { toastOptions } from './constants/toast.constants'; import ErrorBoundry from './ErrorBoundry/ErrorBoundry'; @@ -55,7 +56,9 @@ const App: FunctionComponent = () => { - + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/command-button.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/command-button.svg new file mode 100644 index 00000000000..291009a02f3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/command-button.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/control-button.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/control-button.svg new file mode 100644 index 00000000000..eb8c6bce8ba --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/control-button.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/empty-img-default.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/empty-img-default.svg new file mode 100644 index 00000000000..0eb2cee27c2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/empty-img-default.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/k-button.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/k-button.svg new file mode 100644 index 00000000000..e216d0fcd14 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/k-button.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AddIngestion/Steps/ConfigureIngestion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AddIngestion/Steps/ConfigureIngestion.tsx index 030e512c791..0c0e9efc8ea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AddIngestion/Steps/ConfigureIngestion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AddIngestion/Steps/ConfigureIngestion.tsx @@ -17,7 +17,7 @@ import React, { Fragment, useRef, useState } from 'react'; import { FilterPatternEnum } from '../../../enums/filterPattern.enum'; import { ServiceCategory } from '../../../enums/service.enum'; import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; -import { getSeparator, errorMsg } from '../../../utils/CommonUtils'; +import { errorMsg, getSeparator } from '../../../utils/CommonUtils'; import { Button } from '../../buttons/Button/Button'; import FilterPattern from '../../common/FilterPattern/FilterPattern'; import RichTextEditor from '../../common/rich-text-editor/RichTextEditor'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchProvider.tsx new file mode 100644 index 00000000000..69a28cc944d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchProvider.tsx @@ -0,0 +1,134 @@ +/* + * 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 { Modal } from 'antd'; +import { debounce } from 'lodash'; +import { BaseSelectRef } from 'rc-select'; +import React, { + FC, + ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { useHistory } from 'react-router-dom'; +import AppState from '../../AppState'; +import { getExplorePathWithSearch, ROUTES } from '../../constants/constants'; +import { addToRecentSearched } from '../../utils/CommonUtils'; +import { isCommandKeyPress, Keys } from '../../utils/KeyboardUtil'; +import GlobalSearchSuggestions from './GlobalSearchSuggestions/GlobalSearchSuggestions'; + +export const GlobalSearchContext = React.createContext(null); + +interface Props { + children: ReactNode; +} + +const GlobalSearchProvider: FC = ({ children }: Props) => { + const history = useHistory(); + const selectRef = useRef(null); + const [visible, setVisible] = useState(false); + const [searchValue, setSearchValue] = useState(); + const [suggestionSearch, setSuggestionSearch] = useState(''); + + const handleCancel = () => { + setSearchValue(''); + setSuggestionSearch(''); + setVisible(false); + }; + + const handleKeyPress = useCallback((event) => { + if (isCommandKeyPress(event) && event.key === Keys.K) { + setVisible(true); + selectRef.current?.focus(); + } else if (event.key === Keys.ESC) { + handleCancel(); + } + }, []); + + const debouncedOnChange = useCallback( + (text: string): void => { + setSuggestionSearch(text); + }, + [setSuggestionSearch] + ); + + const debounceOnSearch = useCallback(debounce(debouncedOnChange, 400), [ + debouncedOnChange, + ]); + + const searchHandler = (value: string) => { + addToRecentSearched(value); + history.push({ + pathname: getExplorePathWithSearch( + value, + location.pathname.startsWith(ROUTES.EXPLORE) + ? AppState.explorePageTab + : 'tables' + ), + search: location.search, + }); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + const target = e.target as HTMLInputElement; + if (e.key === 'Enter') { + handleCancel(); + searchHandler(target.value); + } else if (e.key === Keys.ESC) { + handleCancel(); + } + }; + + useEffect(() => { + const targetNode = document.body; + targetNode.addEventListener('keydown', handleKeyPress); + + return () => targetNode.removeEventListener('keydown', handleKeyPress); + }, [handleKeyPress]); + + return ( + + {children} + } + footer={null} + transitionName="" + visible={visible} + width={650} + onCancel={handleCancel}> + { + debounceOnSearch(newValue); + setSearchValue(newValue); + }} + /> + + + ); +}; + +export default GlobalSearchProvider; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.interface.ts new file mode 100644 index 00000000000..f3eada69c11 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.interface.ts @@ -0,0 +1,63 @@ +/* + * 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 { BaseSelectRef } from 'rc-select'; + +export interface GlobalSearchSuggestionsProp { + value: string; + searchText: string; + onOptionSelection: () => void; + onInputKeyDown: (e: React.KeyboardEvent) => void; + onSearch: (newValue: string) => void; + selectRef?: React.Ref; +} + +export interface CommonSource { + fullyQualifiedName: string; + serviceType: string; + name: string; +} + +export interface TableSource extends CommonSource { + table_id: string; + table_name: string; +} + +export interface DashboardSource extends CommonSource { + dashboard_id: string; + dashboard_name: string; +} + +export interface TopicSource extends CommonSource { + topic_id: string; + topic_name: string; +} + +export interface PipelineSource extends CommonSource { + pipeline_id: string; + pipeline_name: string; +} + +export interface MlModelSource extends CommonSource { + ml_model_id: string; + mlmodel_name: string; +} + +export interface Option { + _index: string; + _source: TableSource & + DashboardSource & + TopicSource & + PipelineSource & + MlModelSource; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.less b/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.less new file mode 100644 index 00000000000..3a119fb68c6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.less @@ -0,0 +1,19 @@ +.global-search-input { + > .ant-select-selector { + > .ant-select-selection-search { + padding-right: 22px; + } + } +} +.search-grey { + > .ant-input { + background-color: #f8f9fa; + } +} +.global-search-input.ant-select.ant-select-lg { + &:not(.ant-select-costimze-input) { + > .ant-select-selector { + padding: 0px 0px 0px 5px; + } + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.tsx b/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.tsx new file mode 100644 index 00000000000..bc3d62e2fb9 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/GlobalSearchProvider/GlobalSearchSuggestions/GlobalSearchSuggestions.tsx @@ -0,0 +1,333 @@ +/* + * 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 { Select, Typography } from 'antd'; +import { AxiosError, AxiosResponse } from 'axios'; +import React, { useEffect, useRef, useState } from 'react'; +import { Link, useHistory } from 'react-router-dom'; +import { ReactComponent as NoDataFoundSVG } from '../../../assets/svg/empty-img-default.svg'; +import { getSuggestions } from '../../../axiosAPIs/miscAPI'; +import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; +import { FqnPart } from '../../../enums/entity.enum'; +import { SearchIndex } from '../../../enums/search.enum'; +import jsonData from '../../../jsons/en'; +import { getPartialNameFromTableFQN } from '../../../utils/CommonUtils'; +import { serviceTypeLogo } from '../../../utils/ServiceUtils'; +import SVGIcons, { Icons } from '../../../utils/SvgUtils'; +import { getEntityLink } from '../../../utils/TableUtils'; +import { showErrorToast } from '../../../utils/ToastUtils'; +import CmdKIcon from '../../common/CmdKIcon/CmdKIcon.component'; +import { + DashboardSource, + GlobalSearchSuggestionsProp, + MlModelSource, + Option, + PipelineSource, + TableSource, + TopicSource, +} from './GlobalSearchSuggestions.interface'; +import './GlobalSearchSuggestions.less'; + +const GlobalSearchSuggestions = ({ + searchText, + onOptionSelection, + value, + onInputKeyDown, + onSearch, + selectRef, +}: GlobalSearchSuggestionsProp) => { + const history = useHistory(); + const [options, setOptions] = useState>([]); + const [tableSuggestions, setTableSuggestions] = useState([]); + const [topicSuggestions, setTopicSuggestions] = useState([]); + const [dashboardSuggestions, setDashboardSuggestions] = useState< + DashboardSource[] + >([]); + + const [pipelineSuggestions, setPipelineSuggestions] = useState< + PipelineSource[] + >([]); + const [mlModelSuggestions, setMlModelSuggestions] = useState( + [] + ); + const isMounting = useRef(true); + + const setSuggestions = (options: Array