yarn install
test: yarn-install
- yarn test --watchAll --bail --ci
+ yarn test --no-watchAll --bail --ci
build: test
yarn build
"scripts": {
"start": "react-scripts-ts start",
"build": "REACT_APP_BUILD_NUMBER=$BUILD_NUMBER REACT_APP_GIT_COMMIT=$GIT_COMMIT react-scripts-ts build",
- "test": "react-scripts-ts test --env=jsdom",
+ "test": "CI=true react-scripts-ts test --env=jsdom",
"eject": "react-scripts-ts eject",
"lint": "tslint src/** -t verbose"
},
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WrappedFieldProps } from 'redux-form';
+import { FormControlLabel, Checkbox } from '@material-ui/core';
+
+export const CheckboxField = (props: WrappedFieldProps & { label?: string }) =>
+ <FormControlLabel
+ control={
+ <Checkbox
+ checked={props.input.value}
+ onChange={props.input.onChange}
+ disabled={props.meta.submitting}
+ color="primary" />
+ }
+ label={props.label}
+ />;
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
-export class SearchQueriesService {
+export class SearchService {
private recentQueries: string[] = this.getRecentQueries();
+ private savedQueries: string[] = this.getSavedQueries();
saveRecentQuery(query: string) {
- if (this.recentQueries.length >= 5) {
+ if (this.recentQueries.length >= MAX_NUMBER_OF_RECENT_QUERIES) {
this.recentQueries.shift();
this.recentQueries.push(query);
} else {
getRecentQueries() {
return JSON.parse(localStorage.getItem('recentQueries') || '[]') as string[];
}
-}
\ No newline at end of file
+
+ saveQuery(query: string) {
+ this.savedQueries.push(query);
+ localStorage.setItem('savedQueries', JSON.stringify(this.savedQueries));
+ }
+
+ getSavedQueries() {
+ return JSON.parse(localStorage.getItem('savedQueries') || '[]') as string[];
+ }
+
+ deleteSavedQuery(id: number) {
+ this.savedQueries.splice(id, 1);
+ localStorage.setItem('savedQueries', JSON.stringify(this.savedQueries));
+ }
+}
+
+const MAX_NUMBER_OF_RECENT_QUERIES = 5;
\ No newline at end of file
import { LogService } from './log-service/log-service';
import { ApiActions } from "~/services/api/api-actions";
import { WorkflowService } from "~/services/workflow-service/workflow-service";
-import { SearchQueriesService } from '~/services/search-service/search-service';
+import { SearchService } from '~/services/search-service/search-service';
export type ServiceRepository = ReturnType<typeof createServices>;
const collectionFilesService = new CollectionFilesService(collectionService);
const favoriteService = new FavoriteService(linkService, groupsService);
const tagService = new TagService(linkService);
- const searchQueriesService = new SearchQueriesService();
+ const searchService = new SearchService();
return {
ancestorsService,
linkService,
logService,
projectService,
- searchQueriesService,
+ searchService,
tagService,
userService,
webdavClient,
OPEN_SEARCH_VIEW: ofType<{}>(),
CLOSE_SEARCH_VIEW: ofType<{}>(),
SET_SEARCH_RESULTS: ofType<GroupContentsResource[]>(),
- SET_SEARCH_VALUE: ofType<string>()
+ SET_SEARCH_VALUE: ofType<string>(),
+ SET_SAVED_QUERIES: ofType<string[]>()
});
export type SearchBarActions = UnionOf<typeof searchBarActions>;
type?: GroupContentsResource;
cluster?: string;
project?: string;
+ inTrash: boolean;
dataFrom: string;
dataTo: string;
+ saveQuery: boolean;
searchQuery: string;
}
export const goToView = (currentView: string) => searchBarActions.SET_CURRENT_VIEW(currentView);
export const saveRecentQuery = (query: string) =>
- (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- services.searchQueriesService.saveRecentQuery(query);
- };
+ (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) =>
+ services.searchService.saveRecentQuery(query);
+
export const loadRecentQueries = () =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- const recentSearchQueries = services.searchQueriesService.getRecentQueries();
+ const recentSearchQueries = services.searchService.getRecentQueries();
return recentSearchQueries || [];
};
+export const saveQuery = (data: SearchBarAdvanceFormData) =>
+ (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ if (data.saveQuery && data.searchQuery) {
+ services.searchService.saveQuery(data.searchQuery);
+ dispatch(searchBarActions.SET_SAVED_QUERIES(services.searchService.getSavedQueries()));
+ }
+ };
+
+export const deleteSavedQuery = (id: number) =>
+ (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ services.searchService.deleteSavedQuery(id);
+ const savedSearchQueries = services.searchService.getSavedQueries();
+ dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
+ return savedSearchQueries || [];
+ };
+
+export const openSearchView = () =>
+ (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(searchBarActions.OPEN_SEARCH_VIEW());
+ const savedSearchQueries = services.searchService.getSavedQueries();
+ dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
+ };
+
+
export const searchData = (searchValue: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
open: boolean;
searchResults: GroupContentsResource[];
searchValue: string;
+ savedQueries: string[];
}
export enum SearchView {
currentView: SearchView.BASIC,
open: false,
searchResults: [],
- searchValue: ''
+ searchValue: '',
+ savedQueries: ['']
};
export const searchBarReducer = (state = initialState, action: SearchBarActions): SearchBar =>
CLOSE_SEARCH_VIEW: () => ({ ...state, open: false }),
SET_SEARCH_RESULTS: (searchResults) => ({ ...state, searchResults }),
SET_SEARCH_VALUE: (searchValue) => ({ ...state, searchValue }),
+ SET_SAVED_QUERIES: savedQueries => ({ ...state, savedQueries }),
default: () => state
});
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { Field } from "redux-form";
+import { Field } from 'redux-form';
import { TextField } from "~/components/text-field/text-field";
-import { Checkbox, FormControlLabel } from '@material-ui/core';
+import { CheckboxField } from '~/components/checkbox-field/checkbox-field';
export const SearchBarTypeField = () =>
<Field
label="Project name" />;
export const SearchBarTrashField = () =>
- <FormControlLabel
- control={
- <Checkbox
- checked={false}
- value="true"
- color="primary"
- />
- }
+ <Field
+ name='inTrash'
+ component={CheckboxField}
label="In trash" />;
export const SearchBarDataFromField = () =>
label="Value" />;
export const SearchBarSaveSearchField = () =>
- <FormControlLabel
- control={
- <Checkbox
- checked={false}
- value="true"
- color="primary"
- />
- }
+ <Field
+ name='saveQuery'
+ component={CheckboxField}
label="Save search query" />;
export const SearchBarQuerySearchField = () =>
import { compose, Dispatch } from 'redux';
import { Paper, StyleRulesCallback, withStyles, WithStyles, Button, Grid, IconButton, CircularProgress } from '@material-ui/core';
import { SearchView } from '~/store/search-bar/search-bar-reducer';
-import { SEARCH_BAR_ADVANCE_FORM_NAME, SearchBarAdvanceFormData } from '~/store/search-bar/search-bar-actions';
+import { SEARCH_BAR_ADVANCE_FORM_NAME, SearchBarAdvanceFormData, saveQuery } from '~/store/search-bar/search-bar-actions';
import { ArvadosTheme } from '~/common/custom-theme';
import { CloseIcon } from '~/components/icon/icon';
import {
SearchBarSaveSearchField, SearchBarQuerySearchField
} from '~/views-components/form-fields/search-bar-form-fields';
-type CssRules = 'form' | 'container' | 'closeIcon' | 'label' | 'buttonWrapper' | 'button' | 'circularProgress';
+type CssRules = 'form' | 'container' | 'closeIcon' | 'label' | 'buttonWrapper' | 'button' | 'circularProgress' | 'searchView';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
form: {
left: 0,
right: 0,
margin: 'auto'
+ },
+ searchView: {
+ color: theme.palette.common.black,
+ borderRadius: `0 0 ${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px`
}
});
interface SearchBarAdvancedViewActionProps {
setView: (currentView: string) => void;
+ saveQuery: (data: SearchBarAdvanceFormData) => void;
}
type SearchBarAdvancedViewProps = SearchBarAdvancedViewActionProps & SearchBarAdvancedViewDataProps
reduxForm<SearchBarAdvanceFormData, SearchBarAdvancedViewActionProps>({
form: SEARCH_BAR_ADVANCE_FORM_NAME,
onSubmit: (data: SearchBarAdvanceFormData, dispatch: Dispatch) => {
+ dispatch<any>(saveQuery(data));
dispatch(reset(SEARCH_BAR_ADVANCE_FORM_NAME));
}
}),
withStyles(styles))(
({ classes, setView, handleSubmit, invalid, submitting, pristine }: SearchBarAdvancedViewProps) =>
- <Paper>
+ <Paper className={classes.searchView}>
<form onSubmit={handleSubmit} className={classes.form}>
<Grid container direction="column" justify="flex-start" alignItems="flex-start">
<Grid item xs={12} container className={classes.container}>
padding: 0
},
searchView: {
- borderRadius: `0 0 ${theme.spacing.unit / 4}px ${theme.spacing.unit / 4}px`
+ borderRadius: `0 0 ${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px`
}
};
};
paddingRight: theme.spacing.unit * 2,
paddingBottom: theme.spacing.unit,
fontSize: '14px',
- cursor: 'pointer'
+ cursor: 'pointer',
+ color: theme.palette.primary.main
},
searchQueryList: {
padding: `${theme.spacing.unit / 2}px ${theme.spacing.unit}px `,
},
searchView: {
color: theme.palette.common.black,
- borderRadius: `0 0 ${theme.spacing.unit / 4}px ${theme.spacing.unit / 4}px`
+ borderRadius: `0 0 ${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px`
}
};
};
interface SearchBarBasicViewProps {
setView: (currentView: string) => void;
recentQueries: () => string[];
+ deleteSavedQuery: (id: number) => void;
+ savedQueries: string[];
}
export const SearchBarBasicView = withStyles(styles)(
- ({ classes, setView, recentQueries }: SearchBarBasicViewProps & WithStyles<CssRules>) =>
+ ({ classes, setView, recentQueries, deleteSavedQuery, savedQueries }: SearchBarBasicViewProps & WithStyles<CssRules>) =>
<Paper className={classes.searchView}>
- <div className={classes.searchQueryList}>Saved search queries</div>
+ <div className={classes.searchQueryList}>Recent search queries</div>
<List component="nav" className={classes.list}>
- <RenderSavedQueries text="Test" />
- <RenderSavedQueries text="Demo" />
+ {recentQueries().map((query, index) => <RecentQueriesItem key={index} text={query} />)}
</List>
- <div className={classes.searchQueryList}>Recent search queries</div>
+ <div className={classes.searchQueryList}>Saved search queries</div>
<List component="nav" className={classes.list}>
- {recentQueries().map((query, index) => <RecentQueriesItem key={query + index} text={query} />)}
+ {savedQueries.map((query, index) => <RenderSavedQueries key={index} text={query} id={index} deleteSavedQuery={deleteSavedQuery} />)}
</List>
<div className={classes.advanced} onClick={() => setView(SearchView.ADVANCED)}>Advanced search</div>
</Paper>
openView: jest.fn(),
loseView: jest.fn(),
closeView: jest.fn(),
+ saveRecentQuery: jest.fn(),
+ loadRecentQueries: () => ['test'],
saveQuery: jest.fn(),
- loadQueries: () => ['test']
+ deleteSavedQuery: jest.fn(),
+ openSearchView: jest.fn()
});
\ No newline at end of file
import { SearchBarAdvancedView } from '~/views-components/search-bar/search-bar-advanced-view';
import { SearchBarAutocompleteView, SearchBarAutocompleteViewDataProps } from '~/views-components/search-bar/search-bar-autocomplete-view';
import { ArvadosTheme } from '~/common/custom-theme';
+import { SearchBarAdvanceFormData } from '~/store/search-bar/search-bar-actions';
-type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'searchBar' | 'view';
+type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
return {
container: {
position: 'relative',
width: '100%',
- borderRadius: theme.spacing.unit / 4
+ borderRadius: theme.spacing.unit / 2
},
containerSearchViewOpened: {
position: 'relative',
width: '100%',
- borderRadius: `${theme.spacing.unit / 4}px ${theme.spacing.unit / 4}px 0 0`
+ borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`
},
input: {
border: 'none',
padding: `0px ${theme.spacing.unit}px`
},
- searchBar: {
- height: '30px'
- },
view: {
position: 'absolute',
- width: '100%'
+ width: '100%',
+ zIndex: 1
}
};
};
type SearchBarDataProps = {
searchValue: string;
currentView: string;
- open: boolean;
+ isPopoverOpen: boolean;
+ savedQueries: string[];
} & SearchBarAutocompleteViewDataProps;
interface SearchBarActionProps {
onSearch: (value: string) => any;
debounce?: number;
onSetView: (currentView: string) => void;
- openView: () => void;
closeView: () => void;
- saveQuery: (query: string) => void;
- loadQueries: () => string[];
+ saveRecentQuery: (query: string) => void;
+ loadRecentQueries: () => string[];
+ saveQuery: (data: SearchBarAdvanceFormData) => void;
+ deleteSavedQuery: (id: number) => void;
+ openSearchView: () => void;
}
type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
value: string;
}
-interface RenderQueriesProps {
+interface RenderSavedQueriesProps {
+ text: string | JSX.Element;
+ id: number;
+ deleteSavedQuery: (id: number) => void;
+}
+
+interface RenderRecentQueriesProps {
text: string | JSX.Element;
}
-export const RecentQueriesItem = (props: RenderQueriesProps) => {
+export const RecentQueriesItem = (props: RenderRecentQueriesProps) => {
return <ListItem button>
<ListItemText secondary={props.text} />
</ListItem>;
};
-export const RenderSavedQueries = (props: RenderQueriesProps) => {
+export const RenderSavedQueries = (props: RenderSavedQueriesProps) => {
return <ListItem button>
<ListItemText secondary={props.text} />
<ListItemSecondaryAction>
<Tooltip title="Remove">
- <IconButton aria-label="Remove">
+ <IconButton aria-label="Remove" onClick={() => props.deleteSavedQuery(props.id)}>
<RemoveIcon />
</IconButton>
</Tooltip>
timeout: number;
render() {
- const { classes, currentView, openView, closeView, open } = this.props;
+ const { classes, currentView, openSearchView, closeView, isPopoverOpen } = this.props;
return <ClickAwayListener onClickAway={() => closeView()}>
- <Paper className={open ? classes.containerSearchViewOpened : classes.container} >
- <form onSubmit={this.handleSubmit} className={classes.searchBar}>
+ <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
+ <form onSubmit={this.handleSubmit}>
<Input
className={classes.input}
onChange={this.handleChange}
value={this.state.value}
fullWidth={true}
disableUnderline={true}
- onClick={() => openView()}
+ onClick={openSearchView}
endAdornment={
<InputAdornment position="end">
<Tooltip title='Search'>
} />
</form>
<div className={classes.view}>
- {open && this.getView(currentView)}
+ {isPopoverOpen && this.getView(currentView)}
</div>
- </Paper>
+ </Paper >
</ClickAwayListener>;
}
}
getView = (currentView: string) => {
+ const { onSetView, loadRecentQueries, savedQueries, deleteSavedQuery, searchValue, searchResults, saveQuery } = this.props;
switch (currentView) {
case SearchView.BASIC:
- return <SearchBarBasicView setView={this.props.onSetView} recentQueries={this.props.loadQueries} />;
+ return <SearchBarBasicView setView={onSetView} recentQueries={loadRecentQueries} savedQueries={savedQueries} deleteSavedQuery={deleteSavedQuery} />;
case SearchView.ADVANCED:
- return <SearchBarAdvancedView setView={this.props.onSetView} />;
+ return <SearchBarAdvancedView setView={onSetView} saveQuery={saveQuery}/>;
case SearchView.AUTOCOMPLETE:
- return <SearchBarAutocompleteView
- searchResults={this.props.searchResults}
- searchValue={this.props.searchValue} />;
+ return <SearchBarAutocompleteView
+ searchResults={searchResults}
+ searchValue={searchValue} />;
default:
- return <SearchBarBasicView setView={this.props.onSetView} recentQueries={this.props.loadQueries} />;
+ return <SearchBarBasicView setView={onSetView} recentQueries={loadRecentQueries} savedQueries={savedQueries} deleteSavedQuery={deleteSavedQuery} />;
}
}
handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearTimeout(this.timeout);
- this.props.saveQuery(this.state.value);
+ this.props.saveRecentQuery(this.state.value);
this.props.onSearch(this.state.value);
- this.props.loadQueries();
+ this.props.loadRecentQueries();
}
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
import { connect } from 'react-redux';
import { RootState } from '~/store/store';
import { Dispatch } from 'redux';
-import { goToView, searchData, searchBarActions } from '~/store/search-bar/search-bar-actions';
+import {
+ goToView,
+ searchData,
+ searchBarActions,
+ deleteSavedQuery,
+ saveRecentQuery,
+ loadRecentQueries,
+ saveQuery,
+ openSearchView
+} from '~/store/search-bar/search-bar-actions';
import { SearchBarView } from '~/views-components/search-bar/search-bar-view';
-import { saveRecentQuery, loadRecentQueries } from '~/store/search-bar/search-bar-actions';
+import { SearchBarAdvanceFormData } from '~/store/search-bar/search-bar-actions';
const mapStateToProps = ({ searchBar }: RootState) => {
return {
searchValue: searchBar.searchValue,
currentView: searchBar.currentView,
- open: searchBar.open,
- searchResults: searchBar.searchResults
+ isPopoverOpen: searchBar.open,
+ searchResults: searchBar.searchResults,
+ savedQueries: searchBar.savedQueries
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
onSearch: (valueSearch: string) => dispatch<any>(searchData(valueSearch)),
onSetView: (currentView: string) => dispatch(goToView(currentView)),
- openView: () => dispatch<any>(searchBarActions.OPEN_SEARCH_VIEW()),
closeView: () => dispatch<any>(searchBarActions.CLOSE_SEARCH_VIEW()),
- saveQuery: (query: string) => dispatch<any>(saveRecentQuery(query)),
- loadQueries: () => dispatch<any>(loadRecentQueries())
+ saveRecentQuery: (query: string) => dispatch<any>(saveRecentQuery(query)),
+ loadRecentQueries: () => dispatch<any>(loadRecentQueries()),
+ saveQuery: (data: SearchBarAdvanceFormData) => dispatch<any>(saveQuery(data)),
+ deleteSavedQuery: (id: number) => dispatch<any>(deleteSavedQuery(id)),
+ openSearchView: () => dispatch<any>(openSearchView())
});
export const SearchBar = connect(mapStateToProps, mapDispatchToProps)(SearchBarView);
\ No newline at end of file
<Grid container item xs className={classes.root}>
<Grid container item xs className={classes.container}>
<SplitterLayout customClassName={classes.splitter} percentage={true}
- primaryIndex={0} primaryMinSize={15} secondaryInitialSize={85} secondaryMinSize={40}>
+ primaryIndex={0} primaryMinSize={10} secondaryInitialSize={90} secondaryMinSize={40}>
<Grid container item xs component='aside' direction='column' className={classes.asidePanel}>
<SidePanel />
</Grid>
import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
import { WorkflowResource, parseWorkflowDefinition, getWorkflowInputs, getInputLabel, stringifyInputType } from '~/models/workflow';
-export type CssRules = 'root' | 'tab';
+export type CssRules = 'root' | 'tab' | 'inputTab';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
- height: '100%',
+ height: '100%'
},
tab: {
minWidth: '50%'
+ },
+ inputTab: {
+ overflowX: 'auto',
+ overflowY: 'hidden',
+ '&:last-child': {
+ paddingBottom: theme.spacing.unit / 2,
+ }
}
});
messages={['Please select a workflow to see its description.']} />
)}
</CardContent>}
- {value === 1 && <CardContent>
+ {value === 1 && <CardContent className={classes.inputTab}>
{workflow
? this.renderInputsTable()
: <DataTableDefaultView