1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from "react";
6 import { compose } from "redux";
7 import { CustomStyleRulesCallback } from 'common/custom-theme';
8 import { IconButton, Paper, Tooltip, InputAdornment, Input } from "@mui/material";
9 import { WithStyles } from '@mui/styles';
10 import withStyles from '@mui/styles/withStyles';
11 import SearchIcon from "@mui/icons-material/Search";
12 import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
13 import { ArvadosTheme } from "common/custom-theme";
14 import { SearchView } from "store/search-bar/search-bar-reducer";
15 import { SearchBarBasicView, SearchBarBasicViewDataProps, SearchBarBasicViewActionProps } from "views-components/search-bar/search-bar-basic-view";
17 SearchBarAutocompleteView,
18 SearchBarAutocompleteViewDataProps,
19 SearchBarAutocompleteViewActionProps,
20 } from "views-components/search-bar/search-bar-autocomplete-view";
22 SearchBarAdvancedView,
23 SearchBarAdvancedViewDataProps,
24 SearchBarAdvancedViewActionProps,
25 } from "views-components/search-bar/search-bar-advanced-view";
26 import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "common/codes";
27 import { debounce } from "debounce";
28 import { Vocabulary } from "models/vocabulary";
29 import { connectVocabulary } from "../resource-properties-form/property-field-common";
30 import { Session } from "models/session";
32 type CssRules = "container" | "containerSearchViewOpened" | "input" | "view";
34 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
39 borderRadius: theme.spacing(0.5),
40 zIndex: theme.zIndex.modal,
42 containerSearchViewOpened: {
45 borderRadius: `${theme.spacing(0.5)} ${theme.spacing(0.5)} 0 0`,
46 zIndex: theme.zIndex.modal,
60 export type SearchBarDataProps = SearchBarViewDataProps &
61 SearchBarAutocompleteViewDataProps &
62 SearchBarAdvancedViewDataProps &
63 SearchBarBasicViewDataProps;
65 interface SearchBarViewDataProps {
68 isPopoverOpen: boolean;
70 vocabulary?: Vocabulary;
74 export type SearchBarActionProps = SearchBarViewActionProps &
75 SearchBarAutocompleteViewActionProps &
76 SearchBarAdvancedViewActionProps &
77 SearchBarBasicViewActionProps;
79 interface SearchBarViewActionProps {
80 onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
81 onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
82 onSetView: (currentView: string) => void;
83 closeView: () => void;
84 openSearchView: () => void;
85 loadRecentQueries: () => string[];
88 setAdvancedDataFromSearchValue: (search: string, vocabulary?: Vocabulary) => void;
89 searchSingleCluster: (session: Session, searchValue: string) => any;
92 type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
94 const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => {
95 if (e.keyCode === KEY_CODE_DOWN) {
97 if (!props.isPopoverOpen) {
98 props.onSetView(SearchView.AUTOCOMPLETE);
99 props.openSearchView();
103 } else if (e.keyCode === KEY_CODE_UP) {
106 } else if (e.keyCode === KEY_CODE_ESC) {
109 } else if (e.keyCode === KEY_ENTER) {
110 if (props.currentView === SearchView.BASIC) {
112 props.onSearch(props.selectedItem.query);
113 } else if (props.currentView === SearchView.AUTOCOMPLETE) {
114 if (props.selectedItem.id !== props.searchValue) {
116 props.navigateTo(props.selectedItem.id);
122 const handleInputClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
123 if (props.searchValue) {
124 props.onSetView(SearchView.AUTOCOMPLETE);
126 props.onSetView(SearchView.BASIC);
128 props.openSearchView();
131 const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
133 if (props.isPopoverOpen && props.currentView === SearchView.ADVANCED) {
136 props.setAdvancedDataFromSearchValue(props.searchValue, props.vocabulary);
137 props.onSetView(SearchView.ADVANCED);
141 export const SearchBarView = compose(
145 class extends React.Component<SearchBarViewProps> {
147 loggedInSessions: [],
150 debouncedSearch = debounce(() => {
151 this.props.onSearch(this.props.searchValue);
154 handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
155 this.debouncedSearch();
156 this.props.onChange(event);
159 handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
160 this.debouncedSearch.clear();
161 this.props.onSubmit(event);
164 componentDidMount(): void {
165 this.setState({ loggedInSessions: this.props.sessions.filter((ss) => ss.loggedIn && ss.userIsActive)});
168 componentDidUpdate( prevProps: Readonly<SearchBarViewProps>, prevState: Readonly<{loggedInSessions: Session[]}>, snapshot?: any ): void {
169 if (prevProps.sessions !== this.props.sessions) {
170 this.setState({ loggedInSessions: this.props.sessions.filter((ss) => ss.loggedIn)});
172 //if a new session is logged in after a search is started, search the new cluster and append those to the results
173 if(prevState.loggedInSessions.length !== this.state.loggedInSessions.length){
174 const newLogin = this.state.loggedInSessions.filter((ss) => !prevState.loggedInSessions.includes(ss));
175 this.props.searchSingleCluster(newLogin[0], this.props.searchValue);
179 componentWillUnmount() {
180 this.debouncedSearch.clear();
184 const { children, ...props } = this.props;
185 const { classes, isPopoverOpen } = this.props;
187 {isPopoverOpen && <Backdrop onClick={props.closeView} />}
189 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container}>
191 data-cy="searchbar-parent-form"
192 onSubmit={this.handleSubmit}>
194 data-cy="searchbar-input-field"
195 className={classes.input}
196 onChange={this.handleChange}
198 value={props.searchValue}
200 disableUnderline={true}
201 onClick={e => handleInputClick(e, props)}
202 onKeyDown={e => handleKeyDown(e, props)}
204 <InputAdornment position="start">
205 <Tooltip title="Search">
206 <IconButton type="submit" size="large">
213 <InputAdornment position="end">
214 <Tooltip title="Advanced search">
215 <IconButton onClick={e => handleDropdownClick(e, props)} size="large">
216 <ArrowDropDownIcon />
223 <div className={classes.view}>{isPopoverOpen && getView({ ...props })}</div>
230 const getView = (props: SearchBarViewProps) => {
231 switch (props.currentView) {
232 case SearchView.AUTOCOMPLETE:
234 <SearchBarAutocompleteView
235 navigateTo={props.navigateTo}
236 searchResults={props.searchResults}
237 searchValue={props.searchValue}
238 selectedItem={props.selectedItem}
241 case SearchView.ADVANCED:
243 <SearchBarAdvancedView
244 closeAdvanceView={props.closeAdvanceView}
246 saveQuery={props.saveQuery}
252 onSetView={props.onSetView}
253 onSearch={props.onSearch}
254 loadRecentQueries={props.loadRecentQueries}
255 savedQueries={props.savedQueries}
256 deleteSavedQuery={props.deleteSavedQuery}
257 editSavedQuery={props.editSavedQuery}
258 selectedItem={props.selectedItem}
264 const backdropStyles: CustomStyleRulesCallback<"backdrop"> = theme => ({
271 zIndex: theme.zIndex.modal,
275 const Backdrop = compose(withStyles(backdropStyles))(({ classes, ...props }: WithStyles<"backdrop"> & React.HTMLProps<HTMLDivElement>) => (
277 className={classes.backdrop}