From: Michal Klobukowski Date: Wed, 13 Mar 2019 11:08:34 +0000 (+0100) Subject: Update search-results to use new query parser X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/d40a26653d3b3c45f3a703b44e860eb01a14c794?ds=sidebyside Update search-results to use new query parser Feature #14917 Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski --- diff --git a/src/store/search-bar/search-bar-actions.test.ts b/src/store/search-bar/search-bar-actions.test.ts index ea290b4d7d..51a73cc39b 100644 --- a/src/store/search-bar/search-bar-actions.test.ts +++ b/src/store/search-bar/search-bar-actions.test.ts @@ -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"'); }); }); }); diff --git a/src/store/search-bar/search-bar-actions.ts b/src/store/search-bar/search-bar-actions.ts index d6aae926c8..3a6973f5a1 100644 --- a/src/store/search-bar/search-bar-actions.ts +++ b/src/store/search-bar/search-bar-actions.ts @@ -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(), @@ -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[] = 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(); }; diff --git a/src/store/search-results-panel/search-results-middleware-service.ts b/src/store/search-results-panel/search-results-middleware-service.ts index a855dc4676..9d7e3207f0 100644 --- a/src/store/search-results-panel/search-results-middleware-service.ts +++ b/src/store/search-results-panel/search-results-middleware-service.ts @@ -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) => 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) => {