Sfoglia il codice sorgente

Search: Enable filtering dashboards in search by current folder (#16790)

* Added search-query-parser package

* Migrate search input field to react and enable current folter filtering

* Reveiw changes

* FIx tags

* Fix event handlers  passed to html elements directly

* noImplicitAny fix

* Debounce search method in search controller

* Search: have clear reset query as well
Dominik Prokop 6 anni fa
parent
commit
7194c6d9bf

+ 1 - 0
package.json

@@ -218,6 +218,7 @@
     "reselect": "4.0.0",
     "rst2html": "github:thoward/rst2html#990cb89",
     "rxjs": "6.4.0",
+    "search-query-parser": "1.5.2",
     "slate": "0.33.8",
     "slate-plain-serializer": "0.5.41",
     "slate-prism": "0.5.0",

+ 7 - 0
public/app/core/angular_wrappers.ts

@@ -11,6 +11,7 @@ import { MetricSelect } from './components/Select/MetricSelect';
 import AppNotificationList from './components/AppNotifications/AppNotificationList';
 import { ColorPicker, SeriesColorPickerPopoverWithTheme, SecretFormField } from '@grafana/ui';
 import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
+import { SearchField } from './components/search/SearchField';
 
 export function registerAngularDirectives() {
   react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
@@ -20,6 +21,12 @@ export function registerAngularDirectives() {
   react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
   react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
   react2AngularDirective('searchResult', SearchResult, []);
+  react2AngularDirective('searchField', SearchField, [
+    'query',
+    'autoFocus',
+    ['onChange', { watchDepth: 'reference' }],
+    ['onKeyDown', { watchDepth: 'reference' }],
+  ]);
   react2AngularDirective('tagFilter', TagFilter, [
     'tags',
     ['onChange', { watchDepth: 'reference' }],

+ 95 - 0
public/app/core/components/search/SearchField.tsx

@@ -0,0 +1,95 @@
+import React, { useContext } from 'react';
+import tinycolor from 'tinycolor2';
+import { SearchQuery } from './search';
+import { css, cx } from 'emotion';
+import { ThemeContext, GrafanaTheme, selectThemeVariant } from '@grafana/ui';
+
+type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
+
+interface SearchFieldProps extends Omit<React.HTMLAttributes<HTMLInputElement>, 'onChange'> {
+  query: SearchQuery;
+  onChange: (query: string) => void;
+  onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
+}
+
+const getSearchFieldStyles = (theme: GrafanaTheme) => ({
+  wrapper: css`
+    width: 100%;
+    height: 55px; /* this variable is not part of GrafanaTheme yet*/
+    display: flex;
+    background-color: ${selectThemeVariant(
+      {
+        light: theme.colors.white,
+        dark: theme.colors.dark4,
+      },
+      theme.type
+    )};
+    position: relative;
+  `,
+  input: css`
+    max-width: 653px;
+    padding: ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.sm} ${theme.spacing.md};
+    height: 51px;
+    box-sizing: border-box;
+    outline: none;
+    background: ${selectThemeVariant(
+      {
+        light: theme.colors.dark1,
+        dark: theme.colors.black,
+      },
+      theme.type
+    )};
+    background-color: ${selectThemeVariant(
+      {
+        light: tinycolor(theme.colors.white)
+          .lighten(4)
+          .toString(),
+        dark: theme.colors.dark4,
+      },
+      theme.type
+    )};
+    flex-grow: 10;
+  `,
+  spacer: css`
+    flex-grow: 1;
+  `,
+  icon: cx(
+    css`
+      font-size: ${theme.typography.size.lg};
+      padding: ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.sm} ${theme.spacing.md};
+    `,
+    'pointer'
+  ),
+});
+
+export const SearchField: React.FunctionComponent<SearchFieldProps> = ({ query, onChange, ...inputProps }) => {
+  const theme = useContext(ThemeContext);
+  const styles = getSearchFieldStyles(theme);
+
+  return (
+    <>
+      {/* search-field-wrapper class name left on purpose until we migrate entire search to React */}
+      {/* based on it GrafanaCtrl (L256) decides whether or not hide search */}
+      <div className={`${styles.wrapper} search-field-wrapper`}>
+        <div className={styles.icon}>
+          <i className="fa fa-search" />
+        </div>
+
+        <input
+          type="text"
+          placeholder="Find dashboards by name"
+          value={query.query}
+          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+            onChange(event.currentTarget.value);
+          }}
+          tabIndex={1}
+          spellCheck={false}
+          {...inputProps}
+          className={styles.input}
+        />
+
+        <div className={styles.spacer} />
+      </div>
+    </>
+  );
+};

+ 7 - 13
public/app/core/components/search/search.html

@@ -3,19 +3,13 @@
 
 <div class="search-container" ng-if="ctrl.isOpen">
 
-	<div class="search-field-wrapper">
-		<div class="search-field-icon pointer" ng-click="ctrl.closeSearch()"><i class="fa fa-search"></i></div>
+  <search-field
+    query="ctrl.query"
+    autoFocus="ctrl.giveSearchFocus"
+    on-change="ctrl.onQueryChange"
+    on-key-down="ctrl.onKeyDown"
+  />
 
-		<input type="text" placeholder="Find dashboards by name" give-focus="ctrl.giveSearchFocus" tabindex="1"
-						ng-keydown="ctrl.keyDown($event)"
-						ng-model="ctrl.query.query"
-						ng-model-options="{ debounce: 500 }"
-						spellcheck='false'
-						ng-change="ctrl.search()"
-            />
-
-		<div class="search-field-spacer"></div>
-	</div>
 
 	<div class="search-dropdown">
     <div class="search-dropdown__col_1">
@@ -41,7 +35,7 @@
           </a>
         </div>
 
-        <tag-filter tags="ctrl.query.tag" tagOptions="ctrl.getTags" onChange="ctrl.onTagFiltersChanged">
+        <tag-filter tags="ctrl.query.tags" tagOptions="ctrl.getTags" on-change="ctrl.onTagFiltersChanged">
         </tag-filter>
       </div>
 

+ 106 - 28
public/app/core/components/search/search.ts

@@ -1,13 +1,41 @@
-import _ from 'lodash';
+import _, { debounce } from 'lodash';
 import coreModule from '../../core_module';
 import { SearchSrv } from 'app/core/services/search_srv';
 import { contextSrv } from 'app/core/services/context_srv';
+
 import appEvents from 'app/core/app_events';
+import { parse, SearchParserOptions, SearchParserResult } from 'search-query-parser';
+import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
+export interface SearchQuery {
+  query: string;
+  parsedQuery: SearchParserResult;
+  tags: string[];
+  starred: boolean;
+}
+
+class SearchQueryParser {
+  config: SearchParserOptions;
+  constructor(config: SearchParserOptions) {
+    this.config = config;
+  }
+
+  parse(query: string) {
+    const parsedQuery = parse(query, this.config);
+
+    if (typeof parsedQuery === 'string') {
+      return {
+        text: parsedQuery,
+      } as SearchParserResult;
+    }
+
+    return parsedQuery;
+  }
+}
 
 export class SearchCtrl {
   isOpen: boolean;
-  query: any;
-  giveSearchFocus: number;
+  query: SearchQuery;
+  giveSearchFocus: boolean;
   selectedIndex: number;
   results: any;
   currentSearchId: number;
@@ -18,21 +46,48 @@ export class SearchCtrl {
   initialFolderFilterTitle: string;
   isEditor: string;
   hasEditPermissionInFolders: boolean;
+  queryParser: SearchQueryParser;
 
   /** @ngInject */
   constructor($scope, private $location, private $timeout, private searchSrv: SearchSrv) {
     appEvents.on('show-dash-search', this.openSearch.bind(this), $scope);
     appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope);
+    appEvents.on('search-query', debounce(this.search.bind(this), 500), $scope);
 
     this.initialFolderFilterTitle = 'All';
     this.isEditor = contextSrv.isEditor;
     this.hasEditPermissionInFolders = contextSrv.hasEditPermissionInFolders;
+    this.onQueryChange = this.onQueryChange.bind(this);
+    this.onKeyDown = this.onKeyDown.bind(this);
+    this.query = {
+      query: '',
+      parsedQuery: { text: '' },
+      tags: [],
+      starred: false,
+    };
+
+    this.queryParser = new SearchQueryParser({
+      keywords: ['folder'],
+    });
   }
 
   closeSearch() {
     this.isOpen = this.ignoreClose;
   }
 
+  onQueryChange(query: SearchQuery | string) {
+    if (typeof query === 'string') {
+      this.query = {
+        ...this.query,
+        parsedQuery: this.queryParser.parse(query),
+        query: query,
+      };
+    } else {
+      this.query = query;
+    }
+    appEvents.emit('search-query');
+  }
+
   openSearch(evt, payload) {
     if (this.isOpen) {
       this.closeSearch();
@@ -40,10 +95,15 @@ export class SearchCtrl {
     }
 
     this.isOpen = true;
-    this.giveSearchFocus = 0;
+    this.giveSearchFocus = true;
     this.selectedIndex = -1;
     this.results = [];
-    this.query = { query: '', tag: [], starred: false };
+    this.query = {
+      query: evt ? `${evt.query} ` : '',
+      parsedQuery: this.queryParser.parse(evt && evt.query),
+      tags: [],
+      starred: false,
+    };
     this.currentSearchId = 0;
     this.ignoreClose = true;
     this.isLoading = true;
@@ -54,12 +114,12 @@ export class SearchCtrl {
 
     this.$timeout(() => {
       this.ignoreClose = false;
-      this.giveSearchFocus = this.giveSearchFocus + 1;
+      this.giveSearchFocus = true;
       this.search();
     }, 100);
   }
 
-  keyDown(evt) {
+  onKeyDown(evt: KeyboardEvent) {
     if (evt.keyCode === 27) {
       this.closeSearch();
     }
@@ -94,7 +154,7 @@ export class SearchCtrl {
   }
 
   onFilterboxClick() {
-    this.giveSearchFocus = 0;
+    this.giveSearchFocus = false;
     this.preventClose();
   }
 
@@ -155,40 +215,54 @@ export class SearchCtrl {
     this.results[selectedItem.folderIndex].selected = true;
   }
 
-  searchDashboards() {
+  searchDashboards(folderContext?: string) {
     this.currentSearchId = this.currentSearchId + 1;
     const localSearchId = this.currentSearchId;
+    const folderIds = [];
+
+    const { parsedQuery } = this.query;
+
+    if (folderContext === 'current') {
+      folderIds.push(getDashboardSrv().getCurrent().meta.folderId);
+    }
+
     const query = {
       ...this.query,
-      tag: this.query.tag,
+      query: parsedQuery.text,
+      tag: this.query.tags,
+      folderIds,
     };
 
-    return this.searchSrv.search(query).then(results => {
-      if (localSearchId < this.currentSearchId) {
-        return;
-      }
-      this.results = results || [];
-      this.isLoading = false;
-      this.moveSelection(1);
-    });
+    return this.searchSrv
+      .search({
+        ...query,
+      })
+      .then(results => {
+        if (localSearchId < this.currentSearchId) {
+          return;
+        }
+        this.results = results || [];
+        this.isLoading = false;
+        this.moveSelection(1);
+      });
   }
 
   queryHasNoFilters() {
     const query = this.query;
-    return query.query === '' && query.starred === false && query.tag.length === 0;
+    return query.query === '' && query.starred === false && query.tags.length === 0;
   }
 
   filterByTag(tag) {
-    if (_.indexOf(this.query.tag, tag) === -1) {
-      this.query.tag.push(tag);
+    if (_.indexOf(this.query.tags, tag) === -1) {
+      this.query.tags.push(tag);
       this.search();
     }
   }
 
   removeTag(tag, evt) {
-    this.query.tag = _.without(this.query.tag, tag);
+    this.query.tags = _.without(this.query.tags, tag);
     this.search();
-    this.giveSearchFocus = this.giveSearchFocus + 1;
+    this.giveSearchFocus = true;
     evt.stopPropagation();
     evt.preventDefault();
   }
@@ -198,32 +272,36 @@ export class SearchCtrl {
   };
 
   onTagFiltersChanged = (tags: string[]) => {
-    this.query.tag = tags;
+    this.query.tags = tags;
     this.search();
   };
 
   clearSearchFilter() {
-    this.query.tag = [];
+    this.query.query = '';
+    this.query.tags = [];
     this.search();
   }
 
   showStarred() {
     this.query.starred = !this.query.starred;
-    this.giveSearchFocus = this.giveSearchFocus + 1;
+    this.giveSearchFocus = true;
     this.search();
   }
 
   search() {
     this.showImport = false;
     this.selectedIndex = -1;
-    this.searchDashboards();
+    this.searchDashboards(this.query.parsedQuery['folder']);
   }
 
   folderExpanding() {
     this.moveSelection(0);
   }
 
-  private getFlattenedResultForNavigation() {
+  private getFlattenedResultForNavigation(): Array<{
+    folderIndex: number;
+    dashboardIndex: number;
+  }> {
     let folderIndex = 0;
 
     return _.flatMap(this.results, s => {

+ 11 - 3
public/app/features/dashboard/components/DashNav/DashNav.tsx

@@ -61,7 +61,16 @@ export class DashNav extends PureComponent<Props> {
   }
 
   onOpenSearch = () => {
-    appEvents.emit('show-dash-search');
+    const { dashboard } = this.props;
+    const haveFolder = dashboard.meta.folderId > 0;
+    appEvents.emit(
+      'show-dash-search',
+      haveFolder
+        ? {
+            query: 'folder:current',
+          }
+        : null
+    );
   };
 
   onClose = () => {
@@ -142,8 +151,7 @@ export class DashNav extends PureComponent<Props> {
           <a className="navbar-page-btn" onClick={this.onOpenSearch}>
             {!this.isInFullscreenOrSettings && <i className="gicon gicon-dashboard" />}
             {haveFolder && <span className="navbar-page-btn--folder">{folderTitle} / </span>}
-            {dashboard.title}
-            <i className="fa fa-caret-down" />
+            {dashboard.title} <i className="fa fa-caret-down" />
           </a>
         </div>
         {this.isSettings && <span className="navbar-settings-title">&nbsp;/ Settings</span>}

+ 0 - 28
public/sass/components/_search.scss

@@ -19,34 +19,6 @@
 }
 
 // Search
-.search-field-wrapper {
-  width: 100%;
-  height: $navbarHeight;
-  display: flex;
-  background-color: $navbarBackground;
-  position: relative;
-
-  & > input {
-    max-width: 653px;
-    padding: $space-md $space-md $space-sm $space-md;
-    height: 51px;
-    box-sizing: border-box;
-    outline: none;
-    background: $side-menu-bg;
-    background-color: $navbarButtonBackground;
-    flex-grow: 10;
-  }
-}
-
-.search-field-spacer {
-  flex-grow: 1;
-}
-
-.search-field-icon {
-  font-size: $font-size-lg;
-  padding: $space-md $space-md $space-sm $space-md;
-}
-
 .search-dropdown {
   display: flex;
   flex-direction: column;

+ 5 - 0
yarn.lock

@@ -15245,6 +15245,11 @@ scss-tokenizer@^0.2.3:
     js-base64 "^2.1.8"
     source-map "^0.4.2"
 
+search-query-parser@1.5.2:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/search-query-parser/-/search-query-parser-1.5.2.tgz#f6c8c9ecbde439cbbce75110045944c3cb5fe546"
+  integrity sha512-PcvjC0eJMmFIYAxUaeaRVLnPHctzsymtMJUSGKv6xJtctGrunihoCItrQ3AcM5eO7q90pNeIVTrLwuqW0LIzyg==
+
 select-hose@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"