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 { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles, Tooltip, InputAdornment, Input } from "@material-ui/core";
8 import SearchIcon from "@material-ui/icons/Search";
9 import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
10 import { ArvadosTheme } from "common/custom-theme";
11 import { SearchView } from "store/search-bar/search-bar-reducer";
12 import { SearchBarBasicView, SearchBarBasicViewDataProps, SearchBarBasicViewActionProps } from "views-components/search-bar/search-bar-basic-view";
14 SearchBarAutocompleteView,
15 SearchBarAutocompleteViewDataProps,
16 SearchBarAutocompleteViewActionProps,
17 } from "views-components/search-bar/search-bar-autocomplete-view";
19 SearchBarAdvancedView,
20 SearchBarAdvancedViewDataProps,
21 SearchBarAdvancedViewActionProps,
22 } from "views-components/search-bar/search-bar-advanced-view";
23 import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "common/codes";
24 import { debounce } from "debounce";
25 import { Vocabulary } from "models/vocabulary";
26 import { connectVocabulary } from "../resource-properties-form/property-field-common";
27 import { Session } from "models/session";
29 type CssRules = "container" | "containerSearchViewOpened" | "input" | "view";
31 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
36 borderRadius: theme.spacing.unit / 2,
37 zIndex: theme.zIndex.modal,
39 containerSearchViewOpened: {
42 borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`,
43 zIndex: theme.zIndex.modal,
57 export type SearchBarDataProps = SearchBarViewDataProps &
58 SearchBarAutocompleteViewDataProps &
59 SearchBarAdvancedViewDataProps &
60 SearchBarBasicViewDataProps;
62 interface SearchBarViewDataProps {
65 isPopoverOpen: boolean;
67 vocabulary?: Vocabulary;
71 export type SearchBarActionProps = SearchBarViewActionProps &
72 SearchBarAutocompleteViewActionProps &
73 SearchBarAdvancedViewActionProps &
74 SearchBarBasicViewActionProps;
76 interface SearchBarViewActionProps {
77 onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
78 onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
79 onSetView: (currentView: string) => void;
80 closeView: () => void;
81 openSearchView: () => void;
82 loadRecentQueries: () => string[];
85 setAdvancedDataFromSearchValue: (search: string, vocabulary?: Vocabulary) => void;
86 searchSingleCluster: (session: Session, searchValue: string) => any;
89 type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
91 const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => {
92 if (e.keyCode === KEY_CODE_DOWN) {
94 if (!props.isPopoverOpen) {
95 props.onSetView(SearchView.AUTOCOMPLETE);
96 props.openSearchView();
100 } else if (e.keyCode === KEY_CODE_UP) {
103 } else if (e.keyCode === KEY_CODE_ESC) {
106 } else if (e.keyCode === KEY_ENTER) {
107 if (props.currentView === SearchView.BASIC) {
109 props.onSearch(props.selectedItem.query);
110 } else if (props.currentView === SearchView.AUTOCOMPLETE) {
111 if (props.selectedItem.id !== props.searchValue) {
113 props.navigateTo(props.selectedItem.id);
119 const handleInputClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
120 if (props.searchValue) {
121 props.onSetView(SearchView.AUTOCOMPLETE);
123 props.onSetView(SearchView.BASIC);
125 props.openSearchView();
128 const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
130 if (props.isPopoverOpen && props.currentView === SearchView.ADVANCED) {
133 props.setAdvancedDataFromSearchValue(props.searchValue, props.vocabulary);
134 props.onSetView(SearchView.ADVANCED);
138 export const SearchBarView = compose(
142 class extends React.Component<SearchBarViewProps> {
144 loggedInSessions: [],
147 debouncedSearch = debounce(() => {
148 this.props.onSearch(this.props.searchValue);
151 handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
152 this.debouncedSearch();
153 this.props.onChange(event);
156 handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
157 this.debouncedSearch.clear();
158 this.props.onSubmit(event);
161 componentDidMount(): void {
162 this.setState({ loggedInSessions: this.props.sessions.filter((ss) => ss.loggedIn && ss.userIsActive)});
165 componentDidUpdate( prevProps: Readonly<SearchBarViewProps>, prevState: Readonly<{loggedInSessions: Session[]}>, snapshot?: any ): void {
166 if (prevProps.sessions !== this.props.sessions) {
167 this.setState({ loggedInSessions: this.props.sessions.filter((ss) => ss.loggedIn)});
169 //if a new session is logged in after a search is started, search the new cluster and append those to the results
170 if(prevState.loggedInSessions.length !== this.state.loggedInSessions.length){
171 const newLogin = this.state.loggedInSessions.filter((ss) => !prevState.loggedInSessions.includes(ss));
172 this.props.searchSingleCluster(newLogin[0], this.props.searchValue);
176 componentWillUnmount() {
177 this.debouncedSearch.clear();
181 const { children, ...props } = this.props;
182 const { classes, isPopoverOpen } = this.props;
185 {isPopoverOpen && <Backdrop onClick={props.closeView} />}
187 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container}>
189 data-cy="searchbar-parent-form"
190 onSubmit={this.handleSubmit}>
192 data-cy="searchbar-input-field"
193 className={classes.input}
194 onChange={this.handleChange}
196 value={props.searchValue}
198 disableUnderline={true}
199 onClick={e => handleInputClick(e, props)}
200 onKeyDown={e => handleKeyDown(e, props)}
202 <InputAdornment position="start">
203 <Tooltip title="Search">
204 <IconButton type="submit">
211 <InputAdornment position="end">
212 <Tooltip title="Advanced search">
213 <IconButton onClick={e => handleDropdownClick(e, props)}>
214 <ArrowDropDownIcon />
221 <div className={classes.view}>{isPopoverOpen && getView({ ...props })}</div>
229 const getView = (props: SearchBarViewProps) => {
230 switch (props.currentView) {
231 case SearchView.AUTOCOMPLETE:
233 <SearchBarAutocompleteView
234 navigateTo={props.navigateTo}
235 searchResults={props.searchResults}
236 searchValue={props.searchValue}
237 selectedItem={props.selectedItem}
240 case SearchView.ADVANCED:
242 <SearchBarAdvancedView
243 closeAdvanceView={props.closeAdvanceView}
245 saveQuery={props.saveQuery}
251 onSetView={props.onSetView}
252 onSearch={props.onSearch}
253 loadRecentQueries={props.loadRecentQueries}
254 savedQueries={props.savedQueries}
255 deleteSavedQuery={props.deleteSavedQuery}
256 editSavedQuery={props.editSavedQuery}
257 selectedItem={props.selectedItem}
263 const Backdrop = withStyles<"backdrop">(theme => ({
270 zIndex: theme.zIndex.modal,
272 }))(({ classes, ...props }: WithStyles<"backdrop"> & React.HTMLProps<HTMLDivElement>) => (
274 className={classes.backdrop}