diff --git a/package.json b/package.json index 87ab3a09c7e..6b5f01db47b 100644 --- a/package.json +++ b/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", diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 4460d104f6b..6da0ca1a44d 100644 --- a/public/app/core/angular_wrappers.ts +++ b/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' }], diff --git a/public/app/core/components/search/SearchField.tsx b/public/app/core/components/search/SearchField.tsx new file mode 100644 index 00000000000..5b6dd03165f --- /dev/null +++ b/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 = Pick>; + +interface SearchFieldProps extends Omit, 'onChange'> { + query: SearchQuery; + onChange: (query: string) => void; + onKeyDown: (e: React.KeyboardEvent) => 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 = ({ 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 */} +
+
+ +
+ + ) => { + onChange(event.currentTarget.value); + }} + tabIndex={1} + spellCheck={false} + {...inputProps} + className={styles.input} + /> + +
+
+ + ); +}; diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 8a83ecbc205..bf5d4953228 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -3,19 +3,13 @@
-
-
+ - - -
-
@@ -41,7 +35,7 @@
- +
diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index ff63ca5a8fe..9cf8e9cd8c1 100644 --- a/public/app/core/components/search/search.ts +++ b/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 => { diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 50e66e97e7a..d366c5f1831 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -61,7 +61,16 @@ export class DashNav extends PureComponent { } 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 { {!this.isInFullscreenOrSettings && } {haveFolder && {folderTitle} / } - {dashboard.title} - + {dashboard.title}
{this.isSettings &&  / Settings} diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 196075d20b5..1fe950f8e53 100644 --- a/public/sass/components/_search.scss +++ b/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; diff --git a/yarn.lock b/yarn.lock index 277e62192ca..d4893e14f24 100644 --- a/yarn.lock +++ b/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"