merge master
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Tue, 16 Oct 2018 08:49:47 +0000 (10:49 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Tue, 16 Oct 2018 08:49:47 +0000 (10:49 +0200)
Feature #14277

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

22 files changed:
src/components/icon/icon.tsx
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/services/search-service/search-service.ts
src/store/navigation/navigation-action.ts
src/store/search-bar/search-bar-actions.ts
src/store/search-bar/search-bar-reducer.ts
src/store/search-results-panel/search-results-middleware-service.ts [new file with mode: 0644]
src/store/search-results-panel/search-results-panel-actions.ts [new file with mode: 0644]
src/store/store.ts
src/store/trash/trash-actions.ts
src/store/workbench/workbench-actions.ts
src/views-components/search-bar/search-bar-autocomplete-view.tsx
src/views-components/search-bar/search-bar-basic-view.tsx
src/views-components/search-bar/search-bar-view.test.tsx
src/views-components/search-bar/search-bar-view.tsx
src/views-components/search-bar/search-bar.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/search-results-panel/search-results-panel-view.tsx [new file with mode: 0644]
src/views/search-results-panel/search-results-panel.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx
src/views/workflow-panel/workflow-panel-view.tsx

index 5d99aea93b76b12b17b2367ad570f01a4439c6ce..946d81a52a2831bf400fc891ddd9d3ffff365f47 100644 (file)
@@ -9,9 +9,10 @@ import ArrowBack from '@material-ui/icons/ArrowBack';
 import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
 import BubbleChart from '@material-ui/icons/BubbleChart';
 import Cached from '@material-ui/icons/Cached';
+import ChevronLeft from '@material-ui/icons/ChevronLeft';
 import CloudUpload from '@material-ui/icons/CloudUpload';
 import Code from '@material-ui/icons/Code';
-import ChevronLeft from '@material-ui/icons/ChevronLeft';
+import Create from '@material-ui/icons/Create';
 import ImportContacts from '@material-ui/icons/ImportContacts';
 import ChevronRight from '@material-ui/icons/ChevronRight';
 import Close from '@material-ui/icons/Close';
@@ -24,6 +25,7 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
 import Folder from '@material-ui/icons/Folder';
 import GetApp from '@material-ui/icons/GetApp';
 import Help from '@material-ui/icons/Help';
+import HelpOutline from '@material-ui/icons/HelpOutline';
 import Inbox from '@material-ui/icons/Inbox';
 import Info from '@material-ui/icons/Info';
 import Input from '@material-ui/icons/Input';
@@ -46,7 +48,6 @@ import SettingsApplications from '@material-ui/icons/SettingsApplications';
 import SettingsEthernet from '@material-ui/icons/SettingsEthernet';
 import Star from '@material-ui/icons/Star';
 import StarBorder from '@material-ui/icons/StarBorder';
-import HelpOutline from '@material-ui/icons/HelpOutline';
 
 export type IconType = React.SFC<{ className?: string }>;
 
@@ -63,6 +64,7 @@ export const CloudUploadIcon: IconType = (props) => <CloudUpload {...props} />;
 export const DefaultIcon: IconType = (props) => <RateReview {...props} />;
 export const DetailsIcon: IconType = (props) => <Info {...props} />;
 export const DownloadIcon: IconType = (props) => <GetApp {...props} />;
+export const EditSavedQueryIcon: IconType = (props) => <Create {...props} />;
 export const ExpandIcon: IconType = (props) => <ExpandMoreIcon {...props} />;
 export const FavoriteIcon: IconType = (props) => <Star {...props} />;
 export const HelpIcon: IconType = (props) => <Help {...props} />;
index af3bdab4b092ca61781747eb24a91733fdd20dd0..ef9e9ebcef15a01c271e7e6461b348f4e4b8237c 100644 (file)
@@ -4,10 +4,10 @@
 
 import { History, Location } from 'history';
 import { RootStore } from '~/store/store';
-import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute } from './routes';
+import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute } from './routes';
 import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog } from '~/store/workbench/workbench-actions';
 import { navigateToRootProject } from '~/store/navigation/navigation-action';
-import { loadSharedWithMe, loadRunProcess, loadWorkflow } from '../store/workbench/workbench-actions';
+import { loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults } from '~//store/workbench/workbench-actions';
 
 export const addRouteChangeHandlers = (history: History, store: RootStore) => {
     const handler = handleLocationChange(store);
@@ -23,6 +23,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const trashMatch = matchTrashRoute(pathname);
     const processMatch = matchProcessRoute(pathname);
     const processLogMatch = matchProcessLogRoute(pathname);
+    const searchResultsMatch = matchSearchResultsRoute(pathname);
     const sharedWithMeMatch = matchSharedWithMeRoute(pathname);
     const runProcessMatch = matchRunProcessRoute(pathname);
     const workflowMatch = matchWorkflowRoute(pathname);
@@ -47,5 +48,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(loadRunProcess);
     } else if (workflowMatch) {
         store.dispatch(loadWorkflow);
+    } else if (searchResultsMatch) {
+        store.dispatch(loadSearchResults);
     }
 };
index 432cf750cb2f28f24e75d1986fe8064503f3b21e..e5f3493539e3cbaae317e95fc1b38d3f89cb032f 100644 (file)
@@ -18,7 +18,8 @@ export const Routes = {
     PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`,
     SHARED_WITH_ME: '/shared-with-me',
     RUN_PROCESS: '/run-process',
-    WORKFLOWS: '/workflows'
+    WORKFLOWS: '/workflows',
+    SEARCH_RESULTS: '/search-results'
 };
 
 export const getResourceUrl = (uuid: string) => {
@@ -72,3 +73,6 @@ export const matchRunProcessRoute = (route: string) =>
     
 export const matchWorkflowRoute = (route: string) =>
     matchPath<ResourceRouteParams>(route, { path: Routes.WORKFLOWS });
+
+export const matchSearchResultsRoute = (route: string) =>
+    matchPath<ResourceRouteParams>(route, { path: Routes.SEARCH_RESULTS });
index f9392c237a1c46387f49891201e493ca9490cbb3..8a41fbc79db82ba1b33e4f0a4d4d62145370e802 100644 (file)
@@ -2,9 +2,11 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { SearchBarAdvanceFormData } from '~/models/search-bar';
+
 export class SearchService {
     private recentQueries: string[] = this.getRecentQueries();
-    private savedQueries: string[] = this.getSavedQueries();
+    private savedQueries: SearchBarAdvanceFormData[] = this.getSavedQueries();
 
     saveRecentQuery(query: string) {
         if (this.recentQueries.length >= MAX_NUMBER_OF_RECENT_QUERIES) {
@@ -20,13 +22,13 @@ export class SearchService {
         return JSON.parse(localStorage.getItem('recentQueries') || '[]') as string[];
     }
 
-    saveQuery(query: string) {
-        this.savedQueries.push(query);
+    saveQuery(data: SearchBarAdvanceFormData) {
+        this.savedQueries.push({...data});
         localStorage.setItem('savedQueries', JSON.stringify(this.savedQueries));
     }
 
     getSavedQueries() {
-        return JSON.parse(localStorage.getItem('savedQueries') || '[]') as string[];
+        return JSON.parse(localStorage.getItem('savedQueries') || '[]') as SearchBarAdvanceFormData[];
     }
 
     deleteSavedQuery(id: number) {
index 48bd606e41b146d1f73849d27e3957a84ab9fee8..b060afc433c11df2f554ea1160c53f309f16cc68 100644 (file)
@@ -56,4 +56,6 @@ export const navigateToRootProject = (dispatch: Dispatch, getState: () => RootSt
 
 export const navigateToSharedWithMe = push(Routes.SHARED_WITH_ME);
 
-export const navigateToRunProcess = push(Routes.RUN_PROCESS);
\ No newline at end of file
+export const navigateToRunProcess = push(Routes.RUN_PROCESS);
+
+export const navigateToSearchResults = push(Routes.SEARCH_RESULTS);
\ No newline at end of file
index 4ecf0ff438b3821e4b5d69086b8bb26552de1f9a..9ddfc9c5175b0d6331ae471ae32f76f423e908af 100644 (file)
@@ -12,6 +12,10 @@ import { ServiceRepository } from '~/services/services';
 import { FilterBuilder } from "~/services/api/filter-builder";
 import { ResourceKind } from '~/models/resource';
 import { GroupClass } from '~/models/group';
+import { SearchView } from '~/store/search-bar/search-bar-reducer';
+import { navigateToSearchResults, navigateTo } from '~/store/navigation/navigation-action';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { initialize } from 'redux-form';
 import { SearchBarAdvanceFormData, PropertyValues } from '~/models/search-bar';
 
 export const searchBarActions = unionize({
@@ -20,7 +24,7 @@ export const searchBarActions = unionize({
     CLOSE_SEARCH_VIEW: ofType<{}>(),
     SET_SEARCH_RESULTS: ofType<GroupContentsResource[]>(),
     SET_SEARCH_VALUE: ofType<string>(),
-    SET_SAVED_QUERIES: ofType<string[]>()
+    SET_SAVED_QUERIES: ofType<SearchBarAdvanceFormData[]>()
 });
 
 export type SearchBarActions = UnionOf<typeof searchBarActions>;
@@ -45,8 +49,9 @@ export const loadRecentQueries = () =>
 export const saveQuery = (data: SearchBarAdvanceFormData) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         if (data.saveQuery && data.searchQuery) {
-            services.searchService.saveQuery(data.searchQuery);
+            services.searchService.saveQuery(data);
             dispatch(searchBarActions.SET_SAVED_QUERIES(services.searchService.getSavedQueries()));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Query has been sucessfully saved', kind: SnackbarKind.SUCCESS }));
         }
     };
 
@@ -58,6 +63,12 @@ export const deleteSavedQuery = (id: number) =>
         return savedSearchQueries || [];
     };
 
+export const editSavedQuery = (data: SearchBarAdvanceFormData, id: number) =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.ADVANCED));
+        dispatch<any>(initialize(SEARCH_BAR_ADVANCE_FORM_NAME, data));
+    };
+
 export const openSearchView = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(searchBarActions.OPEN_SEARCH_VIEW());
@@ -65,16 +76,27 @@ export const openSearchView = () =>
         dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
     };
 
-export const closeSearchView = () => 
+export const closeSearchView = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const isOpen = getState().searchBar.open;
-        if(isOpen) {
+        if (isOpen) {
             dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
+            dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
         }
     };
 
+export const navigateToItem = (uuid: string) =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
+        dispatch(navigateTo(uuid));
+    };
+
 export const searchData = (searchValue: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const currentView = getState().searchBar.currentView;
+        if (currentView !== SearchView.AUTOCOMPLETE) {
+            dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
+        }
         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
         dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
         if (searchValue) {
@@ -86,9 +108,10 @@ export const searchData = (searchValue: string) =>
             });
             dispatch(searchBarActions.SET_SEARCH_RESULTS(items));
         }
+        dispatch(navigateToSearchResults);
     };
 
-const getFilters = (filterName: string, searchValue: string): string => {
+export const getFilters = (filterName: string, searchValue: string): string => {
     return new FilterBuilder()
         .addIsA("uuid", [ResourceKind.PROJECT, ResourceKind.COLLECTION, ResourceKind.PROCESS])
         .addILike(filterName, searchValue, GroupContentsResourcePrefix.COLLECTION)
index ce2a77ccae1281ce2cc41ec53cbd7c68fb88842a..a95604374825fd1311b21eba8a143297876e5ee0 100644 (file)
@@ -4,13 +4,14 @@
 
 import { searchBarActions, SearchBarActions } from '~/store/search-bar/search-bar-actions';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
+import { SearchBarAdvanceFormData } from '~/models/search-bar';
 
 interface SearchBar {
     currentView: string;
     open: boolean;
     searchResults: GroupContentsResource[];
     searchValue: string;
-    savedQueries: string[];
+    savedQueries: SearchBarAdvanceFormData[];
 }
 
 export enum SearchView {
@@ -24,7 +25,7 @@ const initialState: SearchBar = {
     open: false,
     searchResults: [],
     searchValue: '',
-    savedQueries: ['']
+    savedQueries: []
 };
 
 export const searchBarReducer = (state = initialState, action: SearchBarActions): SearchBar =>
diff --git a/src/store/search-results-panel/search-results-middleware-service.ts b/src/store/search-results-panel/search-results-middleware-service.ts
new file mode 100644 (file)
index 0000000..5ccb61b
--- /dev/null
@@ -0,0 +1,75 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { updateResources } from '~/store/resources/resources-actions';
+import { SortDirection } from '~/components/data-table/data-column';
+import { SearchResultsPanelColumnNames } from '~/views/search-results-panel/search-results-panel-view';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
+import { ListResults } from '~/services/common-service/common-resource-service';
+import { searchResultsPanelActions } from '~/store/search-results-panel/search-results-panel-actions';
+import { getFilters } from '~/store/search-bar/search-bar-actions';
+
+export class SearchResultsMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(private services: ServiceRepository, id: string) {
+        super(id);
+    }
+
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+        const state = api.getState();
+        const userUuid = state.auth.user!.uuid;
+        const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+        const searchValue = state.searchBar.searchValue;
+        try {
+            const response = await this.services.groupsService.contents(userUuid, getParams(dataExplorer, searchValue));
+            api.dispatch(updateResources(response.items));
+            api.dispatch(setItems(response));
+        } catch {
+            api.dispatch(couldNotFetchWorkflows());
+        }
+    }
+}
+
+export const getParams = (dataExplorer: DataExplorer, searchValue: string) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    filters: getFilters('name', searchValue),
+    order: getOrder(dataExplorer)
+});
+
+const getOrder = (dataExplorer: DataExplorer) => {
+    const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+    const order = new OrderBuilder<GroupContentsResource>();
+    if (sortColumn) {
+        const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+            ? OrderDirection.ASC
+            : OrderDirection.DESC;
+
+        const columnName = sortColumn && sortColumn.name === SearchResultsPanelColumnNames.NAME ? "name" : "modifiedAt";
+        return order
+            .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
+            .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
+            .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT)
+            .getOrder();
+    } else {
+        return order.getOrder();
+    }
+};
+
+export const setItems = (listResults: ListResults<GroupContentsResource>) =>
+    searchResultsPanelActions.SET_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
+
+const couldNotFetchWorkflows = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch workflows.',
+        kind: SnackbarKind.ERROR
+    });
\ No newline at end of file
diff --git a/src/store/search-results-panel/search-results-panel-actions.ts b/src/store/search-results-panel/search-results-panel-actions.ts
new file mode 100644 (file)
index 0000000..05da5b3
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+
+export const SEARCH_RESULTS_PANEL_ID = "searchResultsPanel";
+export const searchResultsPanelActions = bindDataExplorerActions(SEARCH_RESULTS_PANEL_ID);
+
+export const loadSearchResultsPanel = () =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(searchResultsPanelActions.REQUEST_ITEMS());
+    };
\ No newline at end of file
index 064faeca59a0cd289ef42c2d29d47b294d3b9db6..7a0d58f8a568f555e7dc633c69eec2ee3642517e 100644 (file)
@@ -40,6 +40,8 @@ import { WorkflowMiddlewareService } from './workflow-panel/workflow-middleware-
 import { WORKFLOW_PANEL_ID } from './workflow-panel/workflow-panel-actions';
 import { appInfoReducer } from '~/store/app-info/app-info-reducer';
 import { searchBarReducer } from './search-bar/search-bar-reducer';
+import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions';
+import { SearchResultsMiddlewareService } from './search-results-panel/search-results-middleware-service';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -62,6 +64,9 @@ export function configureStore(history: History, services: ServiceRepository): R
     const trashPanelMiddleware = dataExplorerMiddleware(
         new TrashPanelMiddlewareService(services, TRASH_PANEL_ID)
     );
+    const searchResultsPanelMiddleware = dataExplorerMiddleware(
+        new SearchResultsMiddlewareService(services, SEARCH_RESULTS_PANEL_ID)
+    );
     const sharedWithMePanelMiddleware = dataExplorerMiddleware(
         new SharedWithMeMiddlewareService(services, SHARED_WITH_ME_PANEL_ID)
     );
@@ -75,6 +80,7 @@ export function configureStore(history: History, services: ServiceRepository): R
         projectPanelMiddleware,
         favoritePanelMiddleware,
         trashPanelMiddleware,
+        searchResultsPanelMiddleware,
         sharedWithMePanelMiddleware,
         workflowPanelMiddleware
     ];
index b59276c1f2bea84454a48fc11f28d247fa6b4913..92d01582ab1b6cb3db8dcd3c2622636559a4269b 100644 (file)
@@ -10,8 +10,8 @@ import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
 import { activateSidePanelTreeItem, loadSidePanelTreeProjects } from "~/store/side-panel-tree/side-panel-tree-actions";
 import { projectPanelActions } from "~/store/project-panel/project-panel-action";
 import { ResourceKind } from "~/models/resource";
-import { navigateToTrash } from '../navigation/navigation-action';
-import { matchTrashRoute, matchCollectionRoute } from '../../routes/routes';
+import { navigateToTrash } from '~/store/navigation/navigation-action';
+import { matchCollectionRoute } from '~/routes/routes';
 
 export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed: boolean) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
index bc2a9ed688a5100f3695c0c141e1ca7917d45104..1f31656af2c8518b1122a8707e5b8212a47d3879 100644 (file)
@@ -51,6 +51,8 @@ import { loadCollectionFiles } from '~/store/collection-panel/collection-panel-f
 import { SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
 import { CollectionResource } from "~/models/collection";
+import { searchResultsPanelActions, loadSearchResultsPanel } from '~/store/search-results-panel/search-results-panel-actions';
+import { searchResultsPanelColumns } from '~/views/search-results-panel/search-results-panel-view';
 
 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
 
@@ -84,6 +86,7 @@ export const loadWorkbench = () =>
                 dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
                 dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
                 dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
+                dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns }));
                 dispatch<any>(initSidePanelTree());
                 if (router.location) {
                     const match = matchRootRoute(router.location.pathname);
@@ -373,6 +376,11 @@ export const loadWorkflow = handleFirstTimeLoad(async (dispatch: Dispatch<any>)
     dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.WORKFLOWS));
 });
 
+export const loadSearchResults = handleFirstTimeLoad(
+    async (dispatch: Dispatch<any>) => {
+        await dispatch(loadSearchResultsPanel());
+    });
+
 const finishLoadingProject = (project: GroupContentsResource | string) =>
     async (dispatch: Dispatch<any>) => {
         const uuid = typeof project === 'string' ? project : project.uuid;
index 8529450902bcfbc3d8ec628908e3f006c9a17d67..48172424ce2685bd12a6183559bb7e458339f7a3 100644 (file)
@@ -4,7 +4,7 @@
 
 import * as React from 'react';
 import { Paper, StyleRulesCallback, withStyles, WithStyles, List } from '@material-ui/core';
-import { RecentQueriesItem } from '~/views-components/search-bar/search-bar-view';
+import { RenderAutocompleteItems } from '~/views-components/search-bar/search-bar-view';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
 import Highlighter from "react-highlight-words";
 
@@ -24,16 +24,17 @@ const styles: StyleRulesCallback<CssRules> = theme => {
 export interface SearchBarAutocompleteViewDataProps {
     searchResults?: GroupContentsResource[];
     searchValue?: string;
+    navigateTo: (uuid: string) => void;
 }
 
 type SearchBarAutocompleteViewProps = SearchBarAutocompleteViewDataProps & WithStyles<CssRules>;
 
 export const SearchBarAutocompleteView = withStyles(styles)(
-    ({ classes, searchResults, searchValue }: SearchBarAutocompleteViewProps) =>
+    ({ classes, searchResults, searchValue, navigateTo }: SearchBarAutocompleteViewProps) =>
         <Paper className={classes.searchView}>
             {searchResults && <List component="nav" className={classes.list}>
                 {searchResults.map((item: GroupContentsResource) => {
-                    return <RecentQueriesItem key={item.uuid} text={getFormattedText(item.name, searchValue)} />;
+                    return <RenderAutocompleteItems key={item.uuid} text={getFormattedText(item.name, searchValue)} navigateTo={navigateTo} uuid={item.uuid} />;
                 })}
             </List>}
         </Paper>
index a191b2eec7da34fd0243d80b2dcc0c44dd3ae3e9..8ad0f09579d032eed4debfa6e70e851238be3ab6 100644 (file)
@@ -5,7 +5,8 @@
 import * as React from 'react';
 import { Paper, StyleRulesCallback, withStyles, WithStyles, List } from '@material-ui/core';
 import { SearchView } from '~/store/search-bar/search-bar-reducer';
-import { RecentQueriesItem, RenderSavedQueries } from '~/views-components/search-bar/search-bar-view';
+import { RenderRecentQueries, RenderSavedQueries } from '~/views-components/search-bar/search-bar-view';
+import { SearchBarAdvanceFormData } from '~/models/search-bar';
 
 type CssRules = 'advanced' | 'searchQueryList' | 'list' | 'searchView';
 
@@ -39,19 +40,21 @@ interface SearchBarBasicViewProps {
     setView: (currentView: string) => void;
     recentQueries: () => string[];
     deleteSavedQuery: (id: number) => void;
-    savedQueries: string[];
+    savedQueries: SearchBarAdvanceFormData[];
+    onSearch: (searchValue: string) => void;
+    editSavedQuery: (data: SearchBarAdvanceFormData, id: number) => void;
 }
 
 export const SearchBarBasicView = withStyles(styles)(
-    ({ classes, setView, recentQueries, deleteSavedQuery, savedQueries }: SearchBarBasicViewProps & WithStyles<CssRules>) =>
+    ({ classes, setView, recentQueries, deleteSavedQuery, savedQueries, onSearch, editSavedQuery }: SearchBarBasicViewProps & WithStyles<CssRules>) =>
         <Paper className={classes.searchView}>
             <div className={classes.searchQueryList}>Recent search queries</div>
             <List component="nav" className={classes.list}>
-                {recentQueries().map((query, index) => <RecentQueriesItem key={index} text={query} />)}
+                {recentQueries().map((query, index) => <RenderRecentQueries key={index} text={query} onSearch={onSearch} />)}
             </List>
             <div className={classes.searchQueryList}>Saved search queries</div>
             <List component="nav" className={classes.list}>
-                {savedQueries.map((query, index) => <RenderSavedQueries key={index} text={query} id={index} deleteSavedQuery={deleteSavedQuery} />)}
+                {savedQueries.map((query, index) => <RenderSavedQueries key={index} text={query.searchQuery} id={index} deleteSavedQuery={deleteSavedQuery} onSearch={onSearch} data={query} editSavedQuery={editSavedQuery}/>)}
             </List>
             <div className={classes.advanced} onClick={() => setView(SearchView.ADVANCED)}>Advanced search</div>
         </Paper>
index 2ecb835381a9c52ae8308f6d2ce614612223f7f2..801bf7ad3506614f6966406bd2634b19509558de 100644 (file)
@@ -102,5 +102,7 @@ const mockSearchProps = () => ({
     loadRecentQueries: () => ['test'],
     saveQuery: jest.fn(),
     deleteSavedQuery: jest.fn(),
-    openSearchView: jest.fn()
+    openSearchView: jest.fn(),
+    editSavedQuery: jest.fn(),
+    navigateTo: jest.fn()
 });
\ No newline at end of file
index d0967712177eb09effeef030f83c8393a329f950..3d15801ae944a6441f7917a9386cf3199bb65387 100644 (file)
@@ -15,7 +15,7 @@ import {
     ClickAwayListener
 } from '@material-ui/core';
 import SearchIcon from '@material-ui/icons/Search';
-import { RemoveIcon } from '~/components/icon/icon';
+import { RemoveIcon, EditSavedQueryIcon } from '~/components/icon/icon';
 import { SearchView } from '~/store/search-bar/search-bar-reducer';
 import { SearchBarBasicView } from '~/views-components/search-bar/search-bar-basic-view';
 import { SearchBarAdvancedView } from '~/views-components/search-bar/search-bar-advanced-view';
@@ -53,7 +53,7 @@ type SearchBarDataProps = {
     searchValue: string;
     currentView: string;
     isPopoverOpen: boolean;
-    savedQueries: string[];
+    savedQueries: SearchBarAdvanceFormData[];
 } & SearchBarAutocompleteViewDataProps;
 
 interface SearchBarActionProps {
@@ -66,6 +66,8 @@ interface SearchBarActionProps {
     saveQuery: (data: SearchBarAdvanceFormData) => void;
     deleteSavedQuery: (id: number) => void;
     openSearchView: () => void;
+    navigateTo: (uuid: string) => void;
+    editSavedQuery: (data: SearchBarAdvanceFormData, id: number) => void;
 }
 
 type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
@@ -74,27 +76,47 @@ interface SearchBarState {
     value: string;
 }
 
-interface RenderSavedQueriesProps {
-    text: string | JSX.Element;
-    id: number;
-    deleteSavedQuery: (id: number) => void;
+interface RenderRecentQueriesProps {
+    text: string;
+    onSearch: (searchValue: string) => void;
 }
 
-interface RenderRecentQueriesProps {
+export const RenderRecentQueries = (props: RenderRecentQueriesProps) => {
+    return <ListItem button>
+        <ListItemText secondary={props.text} onClick={() => props.onSearch(props.text)} />
+    </ListItem>;
+};
+
+interface RenderAutocompleteItemsProps {
     text: string | JSX.Element;
+    navigateTo: (uuid: string) => void;
+    uuid: string;
 }
 
-export const RecentQueriesItem = (props: RenderRecentQueriesProps) => {
+export const RenderAutocompleteItems = (props: RenderAutocompleteItemsProps) => {
     return <ListItem button>
-        <ListItemText secondary={props.text} />
+        <ListItemText secondary={props.text} onClick={() => props.navigateTo(props.uuid)} />
     </ListItem>;
 };
 
+interface RenderSavedQueriesProps {
+    text: string;
+    id: number;
+    deleteSavedQuery: (id: number) => void;
+    onSearch: (searchValue: string) => void;
+    editSavedQuery: (data: SearchBarAdvanceFormData, id: number) => void;
+    data: SearchBarAdvanceFormData;
+}
 
 export const RenderSavedQueries = (props: RenderSavedQueriesProps) => {
     return <ListItem button>
-        <ListItemText secondary={props.text} />
+        <ListItemText secondary={props.text} onClick={() => props.onSearch(props.text)} />
         <ListItemSecondaryAction>
+            <Tooltip title="Edit">
+                <IconButton aria-label="Edit" onClick={() => props.editSavedQuery(props.data, props.id)}>
+                    <EditSavedQueryIcon />
+                </IconButton>
+            </Tooltip>
             <Tooltip title="Remove">
                 <IconButton aria-label="Remove" onClick={() => props.deleteSavedQuery(props.id)}>
                     <RemoveIcon />
@@ -116,7 +138,7 @@ export const SearchBarView = withStyles(styles)(
 
         render() {
             const { classes, currentView, openSearchView, closeView, isPopoverOpen } = this.props;
-            return <ClickAwayListener onClickAway={() => closeView()}>
+            return <ClickAwayListener onClickAway={closeView}>
                 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
                     <form onSubmit={this.handleSubmit}>
                         <Input
@@ -159,18 +181,19 @@ export const SearchBarView = withStyles(styles)(
         }
 
         getView = (currentView: string) => {
-            const { onSetView, loadRecentQueries, savedQueries, deleteSavedQuery, searchValue, searchResults, saveQuery } = this.props;
+            const { onSetView, loadRecentQueries, savedQueries, deleteSavedQuery, searchValue, searchResults, saveQuery, onSearch, navigateTo, editSavedQuery } = this.props;
             switch (currentView) {
                 case SearchView.BASIC:
-                    return <SearchBarBasicView setView={onSetView} recentQueries={loadRecentQueries} savedQueries={savedQueries} deleteSavedQuery={deleteSavedQuery} />;
+                    return <SearchBarBasicView setView={onSetView} recentQueries={loadRecentQueries} savedQueries={savedQueries} deleteSavedQuery={deleteSavedQuery} onSearch={onSearch} editSavedQuery={editSavedQuery} />;
                 case SearchView.ADVANCED:
-                    return <SearchBarAdvancedView setView={onSetView} saveQuery={saveQuery}/>;
+                    return <SearchBarAdvancedView setView={onSetView} saveQuery={saveQuery} />;
                 case SearchView.AUTOCOMPLETE:
                     return <SearchBarAutocompleteView
+                        navigateTo={navigateTo}
                         searchResults={searchResults}
                         searchValue={searchValue} />;
                 default:
-                    return <SearchBarBasicView setView={onSetView} recentQueries={loadRecentQueries} savedQueries={savedQueries} deleteSavedQuery={deleteSavedQuery} />;
+                    return <SearchBarBasicView setView={onSetView} recentQueries={loadRecentQueries} savedQueries={savedQueries} deleteSavedQuery={deleteSavedQuery} onSearch={onSearch} editSavedQuery={editSavedQuery} />;
             }
         }
 
index d6561c591b770f15c63144120b86549044076ca0..9eae7172de55c2ad6063f982048911638a42b2ad 100644 (file)
@@ -13,7 +13,9 @@ import {
     loadRecentQueries,
     saveQuery,
     openSearchView,
-    closeSearchView
+    closeSearchView,
+    navigateToItem,
+    editSavedQuery
 } from '~/store/search-bar/search-bar-actions';
 import { SearchBarView } from '~/views-components/search-bar/search-bar-view';
 import { SearchBarAdvanceFormData } from '~/models/search-bar';
@@ -36,7 +38,9 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
     loadRecentQueries: () => dispatch<any>(loadRecentQueries()),
     saveQuery: (data: SearchBarAdvanceFormData) => dispatch<any>(saveQuery(data)),
     deleteSavedQuery: (id: number) => dispatch<any>(deleteSavedQuery(id)),
-    openSearchView: () => dispatch<any>(openSearchView())
+    openSearchView: () => dispatch<any>(openSearchView()),
+    navigateTo: (uuid: string) => dispatch<any>(navigateToItem(uuid)),
+    editSavedQuery: (data: SearchBarAdvanceFormData, id: number) => dispatch<any>(editSavedQuery(data, id))
 });
 
 export const SearchBar = connect(mapStateToProps, mapDispatchToProps)(SearchBarView);
\ No newline at end of file
index 4ba967c0529a37b5142f13bc6648e1b3e3b14a01..948885f7081a9e1e691379cf99266e512618bdb2 100644 (file)
@@ -30,7 +30,6 @@ import { navigateTo } from '~/store/navigation/navigation-action';
 import { ContainerRequestState } from "~/models/container-request";
 import { FavoritesState } from '../../store/favorites/favorites-reducer';
 import { RootState } from '~/store/store';
-import { PanelDefaultView } from '~/components/panel-default-view/panel-default-view';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 
 type CssRules = "toolbar" | "button";
diff --git a/src/views/search-results-panel/search-results-panel-view.tsx b/src/views/search-results-panel/search-results-panel-view.tsx
new file mode 100644 (file)
index 0000000..009b2ab
--- /dev/null
@@ -0,0 +1,128 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { SortDirection } from '~/components/data-table/data-column';
+import { DataColumns } from '~/components/data-table/data-table';
+import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
+import { ResourceKind } from '~/models/resource';
+import { ContainerRequestState } from '~/models/container-request';
+import { resourceLabel } from '~/common/labels';
+import { SearchBarAdvanceFormData } from '~/models/search-bar';
+import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions';
+import { DataExplorer } from '~/views-components/data-explorer/data-explorer';
+import {
+    ProcessStatus,
+    ResourceFileSize,
+    ResourceLastModifiedDate,
+    ResourceName,
+    ResourceOwner,
+    ResourceType
+} from '~/views-components/data-explorer/renderers';
+
+export enum SearchResultsPanelColumnNames {
+    NAME = "Name",
+    PROJECT = "Project",
+    STATUS = "Status",
+    TYPE = 'Type',
+    OWNER = "Owner",
+    FILE_SIZE = "File size",
+    LAST_MODIFIED = "Last modified"
+}
+
+export interface SearchResultsPanelDataProps {
+    data: SearchBarAdvanceFormData;
+}
+
+export interface SearchResultsPanelActionProps {
+    onItemClick: (item: string) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
+    onDialogOpen: (ownerUuid: string) => void;
+    onItemDoubleClick: (item: string) => void;
+}
+
+export type SearchResultsPanelProps = SearchResultsPanelDataProps & SearchResultsPanelActionProps;
+
+export interface WorkflowPanelFilter extends DataTableFilterItem {
+    type: ResourceKind | ContainerRequestState;
+}
+
+export const searchResultsPanelColumns: DataColumns<string, WorkflowPanelFilter> = [
+    {
+        name: SearchResultsPanelColumnNames.NAME,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.ASC,
+        filters: [],
+        render: (uuid: string) => <ResourceName uuid={uuid} />
+    },
+    {
+        name: SearchResultsPanelColumnNames.PROJECT,
+        selected: true,
+        configurable: true,
+        filters: [],
+        render: uuid => <ResourceFileSize uuid={uuid} />
+    },
+    {
+        name: SearchResultsPanelColumnNames.STATUS,
+        selected: true,
+        configurable: true,
+        filters: [],
+        render: uuid => <ProcessStatus uuid={uuid} />
+    },
+    {
+        name: SearchResultsPanelColumnNames.TYPE,
+        selected: true,
+        configurable: true,
+        filters: [
+            {
+                name: resourceLabel(ResourceKind.COLLECTION),
+                selected: true,
+                type: ResourceKind.COLLECTION
+            },
+            {
+                name: resourceLabel(ResourceKind.PROCESS),
+                selected: true,
+                type: ResourceKind.PROCESS
+            },
+            {
+                name: resourceLabel(ResourceKind.PROJECT),
+                selected: true,
+                type: ResourceKind.PROJECT
+            }
+        ],
+        render: (uuid: string) => <ResourceType uuid={uuid} />,
+    },
+    {
+        name: SearchResultsPanelColumnNames.OWNER,
+        selected: true,
+        configurable: true,
+        filters: [],
+        render: uuid => <ResourceOwner uuid={uuid} />
+    },
+    {
+        name: SearchResultsPanelColumnNames.FILE_SIZE,
+        selected: true,
+        configurable: true,
+        filters: [],
+        render: uuid => <ResourceFileSize uuid={uuid} />
+    },
+    {
+        name: SearchResultsPanelColumnNames.LAST_MODIFIED,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: [],
+        render: uuid => <ResourceLastModifiedDate uuid={uuid} />
+    }
+];
+
+export const SearchResultsPanelView = (props: SearchResultsPanelProps) => {
+    return <DataExplorer
+        id={SEARCH_RESULTS_PANEL_ID}
+        onRowClick={props.onItemClick}
+        onRowDoubleClick={props.onItemDoubleClick}
+        onContextMenu={props.onContextMenu}
+        contextMenuColumn={true} />;
+};
\ No newline at end of file
diff --git a/src/views/search-results-panel/search-results-panel.tsx b/src/views/search-results-panel/search-results-panel.tsx
new file mode 100644 (file)
index 0000000..fd322d7
--- /dev/null
@@ -0,0 +1,36 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { SearchResultsPanelActionProps } from './search-results-panel-view';
+import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+import { ResourceKind } from '~/models/resource';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { SearchResultsPanelView } from '~/views/search-results-panel/search-results-panel-view';
+
+const mapDispatchToProps = (dispatch: Dispatch): SearchResultsPanelActionProps => ({
+    onContextMenu: (event, resourceUuid) => {
+        const kind = resourceKindToContextMenuKind(resourceUuid);
+        if (kind) {
+            dispatch<any>(openContextMenu(event, {
+                name: '',
+                uuid: resourceUuid,
+                ownerUuid: '',
+                kind: ResourceKind.NONE,
+                menuKind: kind
+            }));
+        }
+    },
+    onDialogOpen: (ownerUuid: string) => { return; },
+    onItemClick: (resourceUuid: string) => {
+        dispatch<any>(loadDetailsPanel(resourceUuid));
+    },
+    onItemDoubleClick: uuid => {
+        dispatch<any>(navigateTo(uuid));
+    }
+});
+
+export const SearchResultsPanel = connect(null, mapDispatchToProps)(SearchResultsPanelView);
\ No newline at end of file
index 09435d096c40ce940ead47f93397a91cf7bf4ea0..788c96ae6b2552500ec3922fdd59748493f0347d 100644 (file)
@@ -41,6 +41,7 @@ import { SharedWithMePanel } from '~/views/shared-with-me-panel/shared-with-me-p
 import { RunProcessPanel } from '~/views/run-process-panel/run-process-panel';
 import SplitterLayout from 'react-splitter-layout';
 import { WorkflowPanel } from '~/views/workflow-panel/workflow-panel';
+import { SearchResultsPanel } from '~/views/search-results-panel/search-results-panel';
 import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tree-picker';
 import { SharedTreePicker } from '~/views-components/projects-tree-picker/shared-tree-picker';
 import { FavoritesTreePicker } from '../../views-components/projects-tree-picker/favorites-tree-picker';
@@ -104,6 +105,7 @@ export const WorkbenchPanel =
                                 <Route path={Routes.SHARED_WITH_ME} component={SharedWithMePanel} />
                                 <Route path={Routes.RUN_PROCESS} component={RunProcessPanel} />
                                 <Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
+                                <Route path={Routes.SEARCH_RESULTS} component={SearchResultsPanel} />
                             </Switch>
                         </Grid>
                     </Grid>
index 57654bc85b12aa0b4ea0abf15aec3a09d9cd524d..cccdce808531bb71aa586f83a835ab5ffb7d34f7 100644 (file)
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { WorkflowIcon } from '~/components/icon/icon';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
-import { WORKFLOW_PANEL_ID, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
+import { WORKFLOW_PANEL_ID } from '~/store/workflow-panel/workflow-panel-actions';
 import {
     ResourceLastModifiedDate,
     RosurceWorkflowName,