Update search-results to use new query parser
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Wed, 13 Mar 2019 11:08:34 +0000 (12:08 +0100)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Wed, 13 Mar 2019 11:08:34 +0000 (12:08 +0100)
Feature #14917

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

src/store/search-bar/search-bar-actions.test.ts
src/store/search-bar/search-bar-actions.ts
src/store/search-results-panel/search-results-middleware-service.ts

index ea290b4d7def5fd3bc35941f39725c1bd2341ec2..51a73cc39b7a3e4e7680457fa522311c4697eefe 100644 (file)
@@ -2,87 +2,23 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { getAdvancedDataFromQuery, getQueryFromAdvancedData, parseSearchQuery } from "~/store/search-bar/search-bar-actions";
+import { getAdvancedDataFromQuery, getQueryFromAdvancedData } from "~/store/search-bar/search-bar-actions";
 import { ResourceKind } from "~/models/resource";
 
 describe('search-bar-actions', () => {
-    describe('parseSearchQuery', () => {
-        it('should correctly parse query #1', () => {
-            const q = 'val0 is:trashed val1';
-            const r = parseSearchQuery(q);
-            expect(r.hasKeywords).toBeTruthy();
-            expect(r.values).toEqual(['val0', 'val1']);
-            expect(r.properties).toEqual({
-                is: ['trashed']
-            });
-        });
-
-        it('should correctly parse query #2 (value with keyword should be ignored)', () => {
-            const q = 'val0 is:from:trashed val1';
-            const r = parseSearchQuery(q);
-            expect(r.hasKeywords).toBeTruthy();
-            expect(r.values).toEqual(['val0', 'val1']);
-            expect(r.properties).toEqual({
-                from: ['trashed']
-            });
-        });
-
-        it('should correctly parse query #3 (many keywords)', () => {
-            const q = 'val0 is:trashed val2 from:2017-04-01 val1';
-            const r = parseSearchQuery(q);
-            expect(r.hasKeywords).toBeTruthy();
-            expect(r.values).toEqual(['val0', 'val2', 'val1']);
-            expect(r.properties).toEqual({
-                is: ['trashed'],
-                from: ['2017-04-01']
-            });
-        });
-
-        it('should correctly parse query #4 (no duplicated values)', () => {
-            const q = 'val0 is:trashed val2 val2 val0';
-            const r = parseSearchQuery(q);
-            expect(r.hasKeywords).toBeTruthy();
-            expect(r.values).toEqual(['val0', 'val2']);
-            expect(r.properties).toEqual({
-                is: ['trashed']
-            });
-        });
-
-        it('should correctly parse query #5 (properties)', () => {
-            const q = 'val0 has:filesize:100mb val2 val2 val0';
-            const r = parseSearchQuery(q);
-            expect(r.hasKeywords).toBeTruthy();
-            expect(r.values).toEqual(['val0', 'val2']);
-            expect(r.properties).toEqual({
-                'has': ['filesize:100mb']
-            });
-        });
-
-        it('should correctly parse query #6 (multiple properties, multiple is)', () => {
-            const q = 'val0 has:filesize:100mb val2 has:user:daniel is:starred val2 val0 is:trashed';
-            const r = parseSearchQuery(q);
-            expect(r.hasKeywords).toBeTruthy();
-            expect(r.values).toEqual(['val0', 'val2']);
-            expect(r.properties).toEqual({
-                'has': ['filesize:100mb', 'user:daniel'],
-                'is': ['starred', 'trashed']
-            });
-        });
-    });
-
     describe('getAdvancedDataFromQuery', () => {
         it('should correctly build advanced data record from query #1', () => {
-            const r = getAdvancedDataFromQuery('val0 has:filesize:100mb val2 has:user:daniel is:starred val2 val0 is:trashed');
+            const r = getAdvancedDataFromQuery('val0 has:"file size":"100mb" val2 has:"user":"daniel" is:starred val2 val0 is:trashed');
             expect(r).toEqual({
                 searchValue: 'val0 val2',
                 type: undefined,
                 cluster: undefined,
                 projectUuid: undefined,
                 inTrash: true,
-                dateFrom: undefined,
-                dateTo: undefined,
+                dateFrom: '',
+                dateTo: '',
                 properties: [{
-                    key: 'filesize',
+                    key: 'file size',
                     value: '100mb'
                 }, {
                     key: 'user',
@@ -94,7 +30,7 @@ describe('search-bar-actions', () => {
         });
 
         it('should correctly build advanced data record from query #2', () => {
-            const r = getAdvancedDataFromQuery('document from:2017-08-01 pdf has:filesize:101mb is:trashed type:arvados#collection cluster:c97qx');
+            const r = getAdvancedDataFromQuery('document from:2017-08-01 pdf has:"filesize":"101mb" is:trashed type:arvados#collection cluster:c97qx');
             expect(r).toEqual({
                 searchValue: 'document pdf',
                 type: ResourceKind.COLLECTION,
@@ -102,7 +38,7 @@ describe('search-bar-actions', () => {
                 projectUuid: undefined,
                 inTrash: true,
                 dateFrom: '2017-08-01',
-                dateTo: undefined,
+                dateTo: '',
                 properties: [{
                     key: 'filesize',
                     value: '101mb'
@@ -124,13 +60,13 @@ describe('search-bar-actions', () => {
                 dateFrom: '2017-08-01',
                 dateTo: '',
                 properties: [{
-                    key: 'filesize',
+                    key: 'file size',
                     value: '101mb'
                 }],
                 saveQuery: false,
                 queryName: ''
             });
-            expect(q).toBe('document pdf type:arvados#collection cluster:c97qx is:trashed from:2017-08-01 has:filesize:101mb');
+            expect(q).toBe('document pdf type:arvados#collection cluster:c97qx is:trashed from:2017-08-01 has:"file size":"101mb"');
         });
     });
 });
index d6aae926c825d197a0946851a35f8cb4f392a3f0..3a6973f5a1388cef736a4cc8c555160ce8f4657b 100644 (file)
@@ -22,6 +22,8 @@ import { activateSearchBarProject } from "~/store/search-bar/search-bar-tree-act
 import { Session } from "~/models/session";
 import { searchResultsPanelActions } from "~/store/search-results-panel/search-results-panel-actions";
 import { ListResults } from "~/services/common-service/common-service";
+import * as parser from './search-query/arv-parser';
+import { Keywords } from './search-query/arv-parser';
 
 export const searchBarActions = unionize({
     SET_CURRENT_VIEW: ofType<string>(),
@@ -210,11 +212,10 @@ const searchGroups = (searchValue: string, limit: number) =>
         const currentView = getState().searchBar.currentView;
 
         if (searchValue || currentView === SearchView.ADVANCED) {
-            const sq = parseSearchQuery(searchValue);
-            const clusterId = getSearchQueryFirstProp(sq, 'cluster');
+            const { cluster: clusterId } = getAdvancedDataFromQuery(searchValue);
             const sessions = getSearchSessions(clusterId, getState().auth.sessions);
             const lists: ListResults<GroupContentsResource>[] = await Promise.all(sessions.map(session => {
-                const filters = searchQueryToFilters(sq);
+                const filters = queryToFilters(searchValue);
                 return services.groupsService.contents('', {
                     filters,
                     limit,
@@ -279,7 +280,7 @@ export const getQueryFromAdvancedData = (data: SearchBarAdvanceFormData, prevDat
         ['type', 'type'],
         ['cluster', 'cluster'],
         ['project', 'projectUuid'],
-        ['is:trashed', 'inTrash'],
+        [`is:${parser.States.TRASHED}`, 'inTrash'],
         ['from', 'dateFrom'],
         ['to', 'dateTo']
     ];
@@ -300,98 +301,18 @@ export const getQueryFromAdvancedData = (data: SearchBarAdvanceFormData, prevDat
     return value;
 };
 
-export class ParseSearchQuery {
-    hasKeywords: boolean;
-    values: string[];
-    properties: {
-        [key: string]: string[]
-    };
-}
-
-export const parseSearchQuery: (query: string) => ParseSearchQuery = (searchValue: string) => {
-    const keywords = [
-        'type:',
-        'cluster:',
-        'project:',
-        'is:',
-        'from:',
-        'to:',
-        'has:'
-    ];
-
-    const hasKeywords = (search: string) => keywords.reduce((acc, keyword) => acc + (search.includes(keyword) ? 1 : 0), 0);
-    let keywordsCnt = 0;
-
-    const properties = {};
-
-    keywords.forEach(k => {
-        if (k) {
-            let p = searchValue.indexOf(k);
-            const key = k.substr(0, k.length - 1);
-
-            while (p >= 0) {
-                const l = searchValue.length;
-                keywordsCnt += 1;
-
-                let v = '';
-                let i = p + k.length;
-                while (i < l && searchValue[i] === ' ') {
-                    ++i;
-                }
-                const vp = i;
-                while (i < l && searchValue[i] !== ' ') {
-                    v += searchValue[i];
-                    ++i;
-                }
-
-                if (hasKeywords(v)) {
-                    searchValue = searchValue.substr(0, p) + searchValue.substr(vp);
-                } else {
-                    if (v !== '') {
-                        if (!properties[key]) {
-                            properties[key] = [];
-                        }
-                        properties[key].push(v);
-                    }
-                    searchValue = searchValue.substr(0, p) + searchValue.substr(i);
-                }
-                p = searchValue.indexOf(k);
-            }
-        }
-    });
-
-    const values = _.uniq(searchValue.split(' ').filter(v => v.length > 0));
-
-    return { hasKeywords: keywordsCnt > 0, values, properties };
-};
-
-export const getSearchQueryFirstProp = (sq: ParseSearchQuery, name: string) => sq.properties[name] && sq.properties[name][0];
-export const getSearchQueryPropValue = (sq: ParseSearchQuery, name: string, value: string) => sq.properties[name] && sq.properties[name].find((v: string) => v === value);
-export const getSearchQueryProperties = (sq: ParseSearchQuery): PropertyValue[] => {
-    if (sq.properties.has) {
-        return sq.properties.has.map((value: string) => {
-            const v = value.split(':');
-            return {
-                key: v[0],
-                value: v[1]
-            };
-        });
-    }
-    return [];
-};
-
 export const getAdvancedDataFromQuery = (query: string): SearchBarAdvanceFormData => {
-    const sq = parseSearchQuery(query);
-
+    const { tokens, searchString } = parser.parseSearchQuery(query);
+    const getValue = parser.getValue(tokens);
     return {
-        searchValue: sq.values.join(' '),
-        type: getSearchQueryFirstProp(sq, 'type') as ResourceKind,
-        cluster: getSearchQueryFirstProp(sq, 'cluster'),
-        projectUuid: getSearchQueryFirstProp(sq, 'project'),
-        inTrash: getSearchQueryPropValue(sq, 'is', 'trashed') !== undefined,
-        dateFrom: getSearchQueryFirstProp(sq, 'from'),
-        dateTo: getSearchQueryFirstProp(sq, 'to'),
-        properties: getSearchQueryProperties(sq),
+        searchValue: searchString,
+        type: getValue(Keywords.TYPE) as ResourceKind,
+        cluster: getValue(Keywords.CLUSTER),
+        projectUuid: getValue(Keywords.PROJECT),
+        inTrash: parser.isTrashed(tokens),
+        dateFrom: getValue(Keywords.FROM) || '',
+        dateTo: getValue(Keywords.TO) || '',
+        properties: parser.getProperties(tokens),
         saveQuery: false,
         queryName: ''
     };
@@ -401,27 +322,24 @@ export const getSearchSessions = (clusterId: string | undefined, sessions: Sessi
     return sessions.filter(s => s.loggedIn && (!clusterId || s.clusterId === clusterId));
 };
 
-export const searchQueryToFilters = (sq: ParseSearchQuery): string => {
+export const queryToFilters = (query: string) => {
+    const data = getAdvancedDataFromQuery(query);
     const filter = new FilterBuilder();
-    const resourceKind = getSearchQueryFirstProp(sq, 'type') as ResourceKind;
+    const resourceKind = data.type;
 
-    const projectUuid = getSearchQueryFirstProp(sq, 'project');
-    if (projectUuid) {
-        filter.addEqual('ownerUuid', projectUuid);
+    if (data.projectUuid) {
+        filter.addEqual('ownerUuid', data.projectUuid);
     }
 
-    const dateFrom = getSearchQueryFirstProp(sq, 'from');
-    if (dateFrom) {
-        filter.addGte('modified_at', buildDateFilter(dateFrom));
+    if (data.dateFrom) {
+        filter.addGte('modified_at', buildDateFilter(data.dateFrom));
     }
 
-    const dateTo = getSearchQueryFirstProp(sq, 'to');
-    if (dateTo) {
-        filter.addLte('modified_at', buildDateFilter(dateTo));
+    if (data.dateTo) {
+        filter.addLte('modified_at', buildDateFilter(data.dateTo));
     }
 
-    const props = getSearchQueryProperties(sq);
-    props.forEach(p => {
+    data.properties.forEach(p => {
         if (p.value) {
             filter
                 .addILike(`properties.${p.key}`, p.value, GroupContentsResourcePrefix.PROJECT)
@@ -432,7 +350,7 @@ export const searchQueryToFilters = (sq: ParseSearchQuery): string => {
 
     return filter
         .addIsA("uuid", buildUuidFilter(resourceKind))
-        .addFullTextSearch(sq.values.join(' '))
+        .addFullTextSearch(data.searchValue)
         .getFilters();
 };
 
index a855dc46760e6d1f56e4ecee9e7239abf26b93e1..9d7e3207f0973379b5ae5cdf4c66baea912c777e 100644 (file)
@@ -16,11 +16,9 @@ import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/g
 import { ListResults } from '~/services/common-service/common-service';
 import { searchResultsPanelActions } from '~/store/search-results-panel/search-results-panel-actions';
 import {
-    getSearchQueryFirstProp,
-    getSearchSessions, ParseSearchQuery,
-    parseSearchQuery,
-    searchQueryToFilters,
-    getSearchQueryPropValue
+    getSearchSessions,
+    queryToFilters,
+    getAdvancedDataFromQuery
 } from '~/store/search-bar/search-bar-actions';
 import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
 import { joinFilters } from '~/services/api/filter-builder';
@@ -39,8 +37,7 @@ export class SearchResultsMiddlewareService extends DataExplorerMiddlewareServic
         const state = api.getState();
         const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
         const searchValue = state.searchBar.searchValue;
-        const sq = parseSearchQuery(searchValue);
-        const clusterId = getSearchQueryFirstProp(sq, 'cluster');
+        const { cluster: clusterId } = getAdvancedDataFromQuery(searchValue);
         const sessions = getSearchSessions(clusterId, state.auth.sessions);
 
         if (searchValue.trim() === '') {
@@ -48,7 +45,7 @@ export class SearchResultsMiddlewareService extends DataExplorerMiddlewareServic
         }
 
         try {
-            const params = getParams(dataExplorer, sq);
+            const params = getParams(dataExplorer, searchValue);
 
             const responses = await Promise.all(sessions.map(session =>
                 this.services.groupsService.contents('', params, session)
@@ -82,14 +79,14 @@ export class SearchResultsMiddlewareService extends DataExplorerMiddlewareServic
 
 const typeFilters = (columns: DataColumns<string>) => serializeResourceTypeFilters(getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE));
 
-export const getParams = (dataExplorer: DataExplorer, sq: ParseSearchQuery) => ({
+export const getParams = (dataExplorer: DataExplorer, query: string) => ({
     ...dataExplorerToListParams(dataExplorer),
     filters: joinFilters(
-        searchQueryToFilters(sq),
+        queryToFilters(query),
         typeFilters(dataExplorer.columns)
     ),
     order: getOrder(dataExplorer),
-    includeTrash: (!!getSearchQueryPropValue(sq, 'is', 'trashed')) || false
+    includeTrash: getAdvancedDataFromQuery(query).inTrash
 });
 
 const getOrder = (dataExplorer: DataExplorer) => {