mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
committed by
Torkel Ödegaard
parent
2e326d1cb8
commit
7194c6d9bf
@@ -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",
|
||||
|
||||
@@ -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
public/app/core/components/search/SearchField.tsx
Normal file
95
public/app/core/components/search/SearchField.tsx
Normal file
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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"> / Settings</span>}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user