mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-14 18:27:35 +00:00
feat(ui): support command + k global search support (#6322)
This commit is contained in:
parent
27558457a7
commit
e102f0eb7c
@ -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();
|
||||
|
@ -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 = () => {
|
||||
<ErrorBoundry>
|
||||
<AuthProvider childComponentType={AppRouter}>
|
||||
<WebSocketProvider>
|
||||
<AppRouter />
|
||||
<GlobalSearchProvider>
|
||||
<AppRouter />
|
||||
</GlobalSearchProvider>
|
||||
</WebSocketProvider>
|
||||
</AuthProvider>
|
||||
</ErrorBoundry>
|
||||
|
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="15" height="15" rx="1.5" stroke="#DDE3EA"/>
|
||||
<path d="M11 9H10V7H11C12.1045 7 13 6.1045 13 5C13 3.8955 12.1045 3 11 3C9.89538 3 9 3.8955 9 5V6H7V5C7 3.8955 6.1045 3 5 3C3.8955 3 3 3.8955 3 5C3 6.1045 3.8955 7 5 7H6V9H5C3.8955 9 3 9.8955 3 11C3 12.1045 3.8955 13 5 13C6.10462 13 7 12.1045 7 11V10H9V10.9975C9 10.9985 8.99988 10.9992 8.99988 11C8.99988 12.1045 9.89538 13 10.9999 13C12.1044 13 12.9999 12.1045 12.9999 11C12.9999 9.8955 12.1045 9.00025 11 9ZM11 4C11.5522 4 12 4.44775 12 5C12 5.55225 11.5522 6 11 6H10V5C10 4.44775 10.4478 4 11 4ZM4 5C4 4.44775 4.44775 4 5 4C5.55225 4 6 4.44775 6 5V6H5C4.44775 6 4 5.55212 4 5ZM5 12C4.44775 12 4 11.5522 4 11C4 10.4478 4.44775 10 5 10H6V11C6 11.5522 5.55225 12 5 12ZM7 9V7H9V9H7ZM11 12C10.4478 12 10 11.5522 10 11V10H10.9999C11.5521 10 11.9999 10.4478 11.9999 11C11.9999 11.5522 11.5522 12 11 12Z" fill="#6B7280"/>
|
||||
</svg>
|
After Width: | Height: | Size: 993 B |
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.12003 11.0881C3.69768 11.0881 3.33404 10.9924 3.02912 10.8011C2.72609 10.608 2.49313 10.3419 2.33026 10.0028C2.16738 9.66383 2.08594 9.27557 2.08594 8.83807C2.08594 8.39489 2.16927 8.00379 2.33594 7.66477C2.5026 7.32386 2.73745 7.05777 3.04048 6.86648C3.34351 6.67519 3.70052 6.57955 4.11151 6.57955C4.44295 6.57955 4.7384 6.6411 4.99787 6.7642C5.25734 6.88542 5.46662 7.05587 5.62571 7.27557C5.7867 7.49527 5.88234 7.75189 5.91264 8.04545H5.08594C5.04048 7.84091 4.93632 7.66477 4.77344 7.51705C4.61245 7.36932 4.39654 7.29545 4.12571 7.29545C3.88897 7.29545 3.68158 7.35795 3.50355 7.48295C3.32741 7.60606 3.1901 7.7822 3.09162 8.01136C2.99313 8.23864 2.94389 8.50758 2.94389 8.81818C2.94389 9.13636 2.99219 9.41098 3.08878 9.64205C3.18537 9.87311 3.32173 10.0521 3.49787 10.179C3.6759 10.3059 3.88518 10.3693 4.12571 10.3693C4.2867 10.3693 4.43253 10.34 4.56321 10.2812C4.69579 10.2206 4.80658 10.1345 4.8956 10.0227C4.98651 9.91098 5.04995 9.77652 5.08594 9.61932H5.91264C5.88234 9.90152 5.79048 10.1534 5.63707 10.375C5.48366 10.5966 5.27817 10.7708 5.0206 10.8977C4.76491 11.0246 4.46473 11.0881 4.12003 11.0881ZM8.85795 6.63636V7.31818H6.47443V6.63636H8.85795ZM7.11364 5.59091H7.96307V9.71875C7.96307 9.88352 7.98769 10.0076 8.03693 10.0909C8.08617 10.1723 8.14962 10.2282 8.22727 10.2585C8.30682 10.2869 8.39299 10.3011 8.4858 10.3011C8.55398 10.3011 8.61364 10.2964 8.66477 10.2869C8.71591 10.2775 8.75568 10.2699 8.78409 10.2642L8.9375 10.9659C8.88826 10.9848 8.81818 11.0038 8.72727 11.0227C8.63636 11.0436 8.52273 11.0549 8.38636 11.0568C8.16288 11.0606 7.95455 11.0208 7.76136 10.9375C7.56818 10.8542 7.41193 10.7254 7.29261 10.5511C7.1733 10.3769 7.11364 10.1581 7.11364 9.89489V5.59091ZM9.79759 11V6.63636H10.6186V7.32955H10.6641C10.7436 7.0947 10.8838 6.91004 11.0845 6.77557C11.2872 6.6392 11.5163 6.57102 11.772 6.57102C11.825 6.57102 11.8875 6.57292 11.9595 6.5767C12.0334 6.58049 12.0911 6.58523 12.1328 6.59091V7.40341C12.0987 7.39394 12.0381 7.38352 11.951 7.37216C11.8639 7.3589 11.7768 7.35227 11.6896 7.35227C11.4889 7.35227 11.3099 7.39489 11.1527 7.48011C10.9974 7.56345 10.8743 7.67992 10.7834 7.82955C10.6925 7.97727 10.647 8.14583 10.647 8.33523V11H9.79759ZM13.7173 5.18182V11H12.8679V5.18182H13.7173Z" fill="#6B7280"/>
|
||||
<rect x="0.5" y="0.5" width="15" height="15" rx="1.5" stroke="#DDE3EA"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,9 @@
|
||||
<svg width="106" height="100" viewBox="0 0 106 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M44.6033 99.4926C69.237 99.4926 89.2066 95.7613 89.2066 91.1584C89.2066 86.5556 69.237 82.8242 44.6033 82.8242C19.9696 82.8242 0 86.5556 0 91.1584C0 95.7613 19.9696 99.4926 44.6033 99.4926Z" fill="#F5F5F7" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.2856 66.6734L64.5455 47.3017C63.7902 46.3899 62.6863 45.8379 61.5238 45.8379H27.6817C26.5198 45.8379 25.4159 46.3899 24.6606 47.3017L8.92114 66.6734V76.7938H80.2863V66.6734H80.2856Z" fill="#AEB8C2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.2566 20.8359H66.9493C67.6473 20.8359 68.3166 21.1132 68.8102 21.6067C69.3037 22.1002 69.5809 22.7696 69.5809 23.4675V84.878C69.5809 85.576 69.3037 86.2453 68.8102 86.7388C68.3166 87.2324 67.6473 87.5096 66.9493 87.5096H22.2566C21.5586 87.5096 20.8893 87.2324 20.3958 86.7388C19.9023 86.2453 19.625 85.576 19.625 84.878V23.4675C19.625 22.7696 19.9023 22.1002 20.3958 21.6067C20.8893 21.1132 21.5586 20.8359 22.2566 20.8359V20.8359Z" fill="#F5F5F7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.0776 27.3848H61.1283C61.4773 27.3848 61.812 27.5234 62.0587 27.7702C62.3055 28.0169 62.4441 28.3516 62.4441 28.7006V45.1196C62.4441 45.4686 62.3055 45.8033 62.0587 46.05C61.812 46.2968 61.4773 46.4354 61.1283 46.4354H28.0776C27.7287 46.4354 27.394 46.2968 27.1472 46.05C26.9005 45.8033 26.7619 45.4686 26.7619 45.1196V28.7006C26.7619 28.3516 26.9005 28.0169 27.1472 27.7702C27.394 27.5234 27.7287 27.3848 28.0776 27.3848V27.3848ZM28.25 53.5782H60.9559C61.3506 53.5782 61.7291 53.735 62.0082 54.0141C62.2873 54.2931 62.4441 54.6717 62.4441 55.0663C62.4441 55.461 62.2873 55.8395 62.0082 56.1186C61.7291 56.3977 61.3506 56.5545 60.9559 56.5545H28.25C27.8553 56.5545 27.4768 56.3977 27.1977 56.1186C26.9186 55.8395 26.7619 55.461 26.7619 55.0663C26.7619 54.6717 26.9186 54.2931 27.1977 54.0141C27.4768 53.735 27.8553 53.5782 28.25 53.5782V53.5782ZM28.25 61.317H60.9559C61.3507 61.317 61.7293 61.4738 62.0085 61.753C62.2876 62.0321 62.4444 62.4107 62.4444 62.8055C62.4444 63.2003 62.2876 63.5789 62.0085 63.858C61.7293 64.1372 61.3507 64.294 60.9559 64.294H28.25C27.8552 64.294 27.4766 64.1372 27.1975 63.858C26.9184 63.5789 26.7615 63.2003 26.7615 62.8055C26.7615 62.4107 26.9184 62.0321 27.1975 61.753C27.4766 61.4738 27.8552 61.317 28.25 61.317V61.317ZM80.1402 89.9367C79.6303 91.9571 77.8395 93.4631 75.7092 93.4631H13.4967C11.3665 93.4631 9.57567 91.9565 9.06646 89.9367C8.96934 89.5517 8.92029 89.1562 8.92041 88.7591V66.6756H26.2349C28.1474 66.6756 29.6888 68.2861 29.6888 70.2413V70.2677C29.6888 72.2223 31.248 73.8006 33.1605 73.8006H56.0454C57.9579 73.8006 59.5171 72.2078 59.5171 70.2525V70.2446C59.5171 68.2894 61.0586 66.6749 62.9711 66.6749H80.2855V88.7598C80.2855 89.1657 80.2349 89.5598 80.1402 89.9367V89.9367Z" fill="#DCE0E6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M82.3164 21.9026L77.823 23.6461C77.7075 23.6909 77.5818 23.7023 77.4602 23.6789C77.3385 23.6555 77.226 23.5982 77.1355 23.5137C77.045 23.4292 76.9802 23.3208 76.9485 23.2011C76.9168 23.0813 76.9196 22.9551 76.9565 22.8368L78.2309 18.7533C76.5276 16.8164 75.5276 14.4546 75.5276 11.9059C75.5276 5.33026 82.1842 0 90.396 0C98.6059 0 105.263 5.33026 105.263 11.9059C105.263 18.4816 98.6065 23.8118 90.3954 23.8118C87.4164 23.8118 84.6427 23.1105 82.3164 21.9026Z" fill="#DCE0E6"/>
|
||||
<path d="M96.2528 14.0555C97.2879 14.0555 98.1271 13.2264 98.1271 12.2035C98.1271 11.1807 97.2879 10.3516 96.2528 10.3516C95.2176 10.3516 94.3784 11.1807 94.3784 12.2035C94.3784 13.2264 95.2176 14.0555 96.2528 14.0555Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M86.4132 13.8248H82.6646L84.5711 10.584L86.4132 13.8248ZM88.756 10.584H92.0356V13.8248H88.756V10.584Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.98295 11V5.18182H6.8608V7.96023H6.93182L9.37216 5.18182H10.4773L8.13068 7.80114L10.4858 11H9.42898L7.5483 8.40057L6.8608 9.19034V11H5.98295Z" fill="#6B7280"/>
|
||||
<rect x="0.5" y="0.5" width="15" height="15" rx="1.5" stroke="#DDE3EA"/>
|
||||
</svg>
|
After Width: | Height: | Size: 346 B |
@ -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';
|
||||
|
@ -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<Props> = ({ children }: Props) => {
|
||||
const history = useHistory();
|
||||
const selectRef = useRef<BaseSelectRef>(null);
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
const [searchValue, setSearchValue] = useState<string>();
|
||||
const [suggestionSearch, setSuggestionSearch] = useState<string>('');
|
||||
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<GlobalSearchContext.Provider value={null}>
|
||||
{children}
|
||||
<Modal
|
||||
closable
|
||||
destroyOnClose
|
||||
maskClosable
|
||||
bodyStyle={{
|
||||
padding: '20px',
|
||||
height: '314px',
|
||||
}}
|
||||
closeIcon={<></>}
|
||||
footer={null}
|
||||
transitionName=""
|
||||
visible={visible}
|
||||
width={650}
|
||||
onCancel={handleCancel}>
|
||||
<GlobalSearchSuggestions
|
||||
searchText={suggestionSearch}
|
||||
selectRef={selectRef}
|
||||
value={searchValue || ''}
|
||||
onInputKeyDown={handleKeyDown}
|
||||
onOptionSelection={handleCancel}
|
||||
onSearch={(newValue) => {
|
||||
debounceOnSearch(newValue);
|
||||
setSearchValue(newValue);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</GlobalSearchContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchProvider;
|
@ -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<HTMLInputElement>) => void;
|
||||
onSearch: (newValue: string) => void;
|
||||
selectRef?: React.Ref<BaseSelectRef>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Array<Option>>([]);
|
||||
const [tableSuggestions, setTableSuggestions] = useState<TableSource[]>([]);
|
||||
const [topicSuggestions, setTopicSuggestions] = useState<TopicSource[]>([]);
|
||||
const [dashboardSuggestions, setDashboardSuggestions] = useState<
|
||||
DashboardSource[]
|
||||
>([]);
|
||||
|
||||
const [pipelineSuggestions, setPipelineSuggestions] = useState<
|
||||
PipelineSource[]
|
||||
>([]);
|
||||
const [mlModelSuggestions, setMlModelSuggestions] = useState<MlModelSource[]>(
|
||||
[]
|
||||
);
|
||||
const isMounting = useRef(true);
|
||||
|
||||
const setSuggestions = (options: Array<Option>) => {
|
||||
setTableSuggestions(
|
||||
options
|
||||
.filter((option) => option._index === SearchIndex.TABLE)
|
||||
.map((option) => option._source)
|
||||
);
|
||||
setTopicSuggestions(
|
||||
options
|
||||
.filter((option) => option._index === SearchIndex.TOPIC)
|
||||
.map((option) => option._source)
|
||||
);
|
||||
setDashboardSuggestions(
|
||||
options
|
||||
.filter((option) => option._index === SearchIndex.DASHBOARD)
|
||||
.map((option) => option._source)
|
||||
);
|
||||
setPipelineSuggestions(
|
||||
options
|
||||
.filter((option) => option._index === SearchIndex.PIPELINE)
|
||||
.map((option) => option._source)
|
||||
);
|
||||
setMlModelSuggestions(
|
||||
options
|
||||
.filter((option) => option._index === SearchIndex.MLMODEL)
|
||||
.map((option) => option._source)
|
||||
);
|
||||
};
|
||||
|
||||
const getGroupLabel = (index: string) => {
|
||||
let label = '';
|
||||
let icon = '';
|
||||
switch (index) {
|
||||
case SearchIndex.TOPIC:
|
||||
label = 'Topics';
|
||||
icon = Icons.TOPIC_GREY;
|
||||
|
||||
break;
|
||||
case SearchIndex.DASHBOARD:
|
||||
label = 'Dashboards';
|
||||
icon = Icons.DASHBOARD_GREY;
|
||||
|
||||
break;
|
||||
case SearchIndex.PIPELINE:
|
||||
label = 'Pipelines';
|
||||
icon = Icons.PIPELINE_GREY;
|
||||
|
||||
break;
|
||||
case SearchIndex.MLMODEL:
|
||||
label = 'ML Models';
|
||||
// TODO: Change this to mlmodel icon
|
||||
icon = Icons.SERVICE;
|
||||
|
||||
break;
|
||||
case SearchIndex.TABLE:
|
||||
default:
|
||||
label = 'Tables';
|
||||
icon = Icons.TABLE_GREY;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select.Option disabled>
|
||||
<div className="tw-flex tw-items-center">
|
||||
<SVGIcons alt="icon" className="tw-h-3 tw-w-4" icon={icon} />
|
||||
<p className="tw-px-2 tw-text-grey-muted tw-text-xs tw-h-4 tw-mb-0">
|
||||
{label}
|
||||
</p>
|
||||
</div>
|
||||
</Select.Option>
|
||||
);
|
||||
};
|
||||
|
||||
const getSuggestionElement = (
|
||||
fqdn: string,
|
||||
serviceType: string,
|
||||
name: string,
|
||||
index: string
|
||||
) => {
|
||||
let database;
|
||||
let schema;
|
||||
if (index === SearchIndex.TABLE) {
|
||||
database = getPartialNameFromTableFQN(fqdn, [FqnPart.Database]);
|
||||
schema = getPartialNameFromTableFQN(fqdn, [FqnPart.Schema]);
|
||||
}
|
||||
const entitiyLink = getEntityLink(index, fqdn);
|
||||
|
||||
return (
|
||||
<Select.Option key={entitiyLink} value={entitiyLink}>
|
||||
<div className="tw-flex tw-items-center" key={fqdn}>
|
||||
<img
|
||||
alt={serviceType}
|
||||
className="tw-inline tw-h-4"
|
||||
src={serviceTypeLogo(serviceType)}
|
||||
/>
|
||||
<Link
|
||||
className="tw-px-2 tw-text-sm"
|
||||
data-testid="data-name"
|
||||
id={fqdn.replace(/\./g, '')}
|
||||
to={entitiyLink}
|
||||
onClick={() => onOptionSelection()}>
|
||||
{database && schema
|
||||
? `${database}${FQN_SEPARATOR_CHAR}${schema}${FQN_SEPARATOR_CHAR}${name}`
|
||||
: name}
|
||||
</Link>
|
||||
</div>
|
||||
</Select.Option>
|
||||
);
|
||||
};
|
||||
|
||||
const getEntitiesSuggestions = () => {
|
||||
return (
|
||||
<>
|
||||
{tableSuggestions.length > 0 && (
|
||||
<>
|
||||
{getGroupLabel(SearchIndex.TABLE)}
|
||||
|
||||
{tableSuggestions.map((suggestion: TableSource) => {
|
||||
const { fullyQualifiedName, name, serviceType } = suggestion;
|
||||
|
||||
return getSuggestionElement(
|
||||
fullyQualifiedName,
|
||||
serviceType,
|
||||
name,
|
||||
SearchIndex.TABLE
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{topicSuggestions.length > 0 && (
|
||||
<>
|
||||
{getGroupLabel(SearchIndex.TOPIC)}
|
||||
|
||||
{topicSuggestions.map((suggestion: TopicSource) => {
|
||||
const { fullyQualifiedName, name, serviceType } = suggestion;
|
||||
|
||||
return getSuggestionElement(
|
||||
fullyQualifiedName,
|
||||
serviceType,
|
||||
name,
|
||||
SearchIndex.TOPIC
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{dashboardSuggestions.length > 0 && (
|
||||
<>
|
||||
{getGroupLabel(SearchIndex.DASHBOARD)}
|
||||
|
||||
{dashboardSuggestions.map((suggestion: DashboardSource) => {
|
||||
const { fullyQualifiedName, name, serviceType } = suggestion;
|
||||
|
||||
return getSuggestionElement(
|
||||
fullyQualifiedName,
|
||||
serviceType,
|
||||
name,
|
||||
SearchIndex.DASHBOARD
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{pipelineSuggestions.length > 0 && (
|
||||
<>
|
||||
{getGroupLabel(SearchIndex.PIPELINE)}
|
||||
|
||||
{pipelineSuggestions.map((suggestion: PipelineSource) => {
|
||||
const { fullyQualifiedName, name, serviceType } = suggestion;
|
||||
|
||||
return getSuggestionElement(
|
||||
fullyQualifiedName,
|
||||
serviceType,
|
||||
name,
|
||||
SearchIndex.PIPELINE
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{mlModelSuggestions.length > 0 && (
|
||||
<>
|
||||
{getGroupLabel(SearchIndex.MLMODEL)}
|
||||
|
||||
{mlModelSuggestions.map((suggestion: MlModelSource) => {
|
||||
const { fullyQualifiedName, name, serviceType } = suggestion;
|
||||
|
||||
return getSuggestionElement(
|
||||
fullyQualifiedName,
|
||||
serviceType,
|
||||
name,
|
||||
SearchIndex.MLMODEL
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounting.current) {
|
||||
getSuggestions(searchText)
|
||||
.then((res: AxiosResponse) => {
|
||||
if (res.data) {
|
||||
setOptions(res.data.suggest['metadata-suggest'][0].options);
|
||||
setSuggestions(res.data.suggest['metadata-suggest'][0].options);
|
||||
} else {
|
||||
throw jsonData['api-error-messages']['unexpected-server-response'];
|
||||
}
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(
|
||||
err,
|
||||
jsonData['api-error-messages']['fetch-suggestions-error']
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [searchText]);
|
||||
|
||||
// always Keep this useEffect at the end...
|
||||
useEffect(() => {
|
||||
isMounting.current = false;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="tw-flex tw-items-center tw-border tw-rounded">
|
||||
<SVGIcons
|
||||
alt={Icons.SEARCHV1}
|
||||
className="tw-ml-2 tw-mr-1 tw-h-4"
|
||||
icon={Icons.SEARCHV1}
|
||||
/>
|
||||
<Select
|
||||
autoFocus
|
||||
open
|
||||
showSearch
|
||||
bordered={false}
|
||||
className="global-search-input"
|
||||
defaultActiveFirstOption={false}
|
||||
dropdownMatchSelectWidth={613}
|
||||
dropdownStyle={{
|
||||
paddingTop: '10px',
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
filterOption={false}
|
||||
listHeight={220}
|
||||
notFoundContent={
|
||||
<div className="tw-flex tw-flex-col tw-w-full tw-h-56 tw-items-center tw-justify-center tw-pb-9">
|
||||
<NoDataFoundSVG />
|
||||
<Typography>No Results Found</Typography>
|
||||
</div>
|
||||
}
|
||||
placeholder="Search"
|
||||
placement="bottomRight"
|
||||
ref={selectRef}
|
||||
size="large"
|
||||
style={{ width: '572px' }}
|
||||
suffixIcon={<CmdKIcon />}
|
||||
value={value}
|
||||
onChange={(value) => {
|
||||
history.push(value);
|
||||
onOptionSelection();
|
||||
}}
|
||||
onInputKeyDown={onInputKeyDown}
|
||||
onSearch={onSearch}>
|
||||
{options.length > 0 ? getEntitiesSuggestions() : null}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchSuggestions;
|
@ -90,7 +90,7 @@ const ConnectionConfigForm: FunctionComponent<Props> = ({
|
||||
}, [serviceType]);
|
||||
|
||||
const config = !isNil(data)
|
||||
? ((data as DataService).connection.config as ConfigData)
|
||||
? ((data as DataService).connection?.config as ConfigData)
|
||||
: ({} as ConfigData);
|
||||
|
||||
const handleSave = (data: ISubmitEvent<ConfigData>) => {
|
||||
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { ReactComponent as CmdButton } from '../../../assets/svg/command-button.svg';
|
||||
import { ReactComponent as CtrlButton } from '../../../assets/svg/control-button.svg';
|
||||
import { ReactComponent as KButton } from '../../../assets/svg/k-button.svg';
|
||||
import { NavigatorHelper } from '../../../utils/NavigatorUtils';
|
||||
|
||||
const CmdKIcon = () => {
|
||||
return (
|
||||
<div className="tw-flex tw-items-center">
|
||||
{NavigatorHelper.isMacOs() ? (
|
||||
<CmdButton
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CtrlButton
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<KButton
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CmdKIcon;
|
@ -121,7 +121,7 @@ const FormBuilder: FunctionComponent<Props> = ({
|
||||
})}
|
||||
formData={localFormData}
|
||||
ref={(form) => {
|
||||
oForm = form;
|
||||
oForm = form as Form<ConfigData>;
|
||||
}}
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
|
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Badge, Dropdown, Space } from 'antd';
|
||||
import { Badge, Dropdown, Input, Space } from 'antd';
|
||||
import { debounce, toString } from 'lodash';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, NavLink, useHistory } from 'react-router-dom';
|
||||
@ -41,6 +41,7 @@ import { getTaskDetailPath } from '../../utils/TasksUtils';
|
||||
import SearchOptions from '../app-bar/SearchOptions';
|
||||
import Suggestions from '../app-bar/Suggestions';
|
||||
import Avatar from '../common/avatar/Avatar';
|
||||
import CmdKIcon from '../common/CmdKIcon/CmdKIcon.component';
|
||||
import PopOver from '../common/popover/PopOver';
|
||||
import DropDown from '../dropdown/DropDown';
|
||||
import { WhatsNewModal } from '../Modals/WhatsNewModal';
|
||||
@ -228,21 +229,31 @@ const NavBar = ({
|
||||
<div
|
||||
className="tw-flex-none tw-relative tw-justify-items-center tw-ml-auto"
|
||||
data-testid="appbar-item">
|
||||
<span
|
||||
className="tw-cursor-pointer tw-absolute tw-right-2.5 tw-top-2 tw-block tw-z-40 tw-w-4 tw-h-4 tw-text-center"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleOnClick();
|
||||
}}>
|
||||
<SVGIcons alt="icon-search" icon={searchIcon} />
|
||||
</span>
|
||||
<input
|
||||
<Input
|
||||
autoComplete="off"
|
||||
className="tw-relative search-grey tw-rounded tw-border tw-border-main focus:tw-outline-none tw-pl-2 tw-pt-2 tw-pb-1.5 tw-form-inputs tw-ml-4"
|
||||
className="tw-relative search-grey hover:tw-outline-none focus:tw-outline-none tw-pl-2 tw-pt-2 tw-pb-1.5 tw-ml-4 tw-z-41"
|
||||
data-testid="searchBox"
|
||||
id="searchBox"
|
||||
placeholder="Search for Table, Topics, Dashboards,Pipeline and ML Models"
|
||||
style={{
|
||||
borderRadius: '0.24rem',
|
||||
boxShadow: 'none',
|
||||
height: '37px',
|
||||
}}
|
||||
suffix={
|
||||
<span
|
||||
className="tw-flex tw-items-center"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleOnClick();
|
||||
}}>
|
||||
<CmdKIcon />
|
||||
<span className="tw-cursor-pointer tw-mb-2 tw-ml-3 tw-w-4 tw-h-4 tw-text-center">
|
||||
<SVGIcons alt="icon-search" icon={searchIcon} />
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
type="text"
|
||||
value={searchValue}
|
||||
onBlur={() => setSearchIcon('icon-searchv1')}
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { KeyboardEvent } from 'react';
|
||||
import { NavigatorHelper } from './NavigatorUtils';
|
||||
|
||||
export enum Keys {
|
||||
K = 'k',
|
||||
ESC = 'Escape',
|
||||
}
|
||||
|
||||
export const isCommandKeyPress = <T extends HTMLInputElement>(
|
||||
event: KeyboardEvent<T>
|
||||
): boolean => {
|
||||
if (NavigatorHelper.isMacOs()) {
|
||||
return event.metaKey;
|
||||
} else if (!NavigatorHelper.isMacOs()) {
|
||||
return event.ctrlKey;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
export enum NavigatorUserAgent {
|
||||
MAC = 'Mac',
|
||||
WINDOWS = 'Win',
|
||||
LINUX = 'Linux',
|
||||
ANDROID = 'Android',
|
||||
IOS = 'like Mac',
|
||||
}
|
||||
|
||||
export class NavigatorHelper {
|
||||
static isMacOs() {
|
||||
return navigator.userAgent.indexOf(NavigatorUserAgent.MAC) != -1;
|
||||
}
|
||||
|
||||
static isWindows() {
|
||||
return navigator.userAgent.indexOf(NavigatorUserAgent.WINDOWS) != -1;
|
||||
}
|
||||
|
||||
static isLinuxOs() {
|
||||
return navigator.userAgent.indexOf(NavigatorUserAgent.LINUX) != -1;
|
||||
}
|
||||
|
||||
static isAndroidOs() {
|
||||
return navigator.userAgent.indexOf(NavigatorUserAgent.ANDROID) != -1;
|
||||
}
|
||||
|
||||
static isIos() {
|
||||
return navigator.userAgent.indexOf(NavigatorUserAgent.IOS) != -1;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user