mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -06:00
* Search: Setup SearchResults.tsx * Search: add watchDepth * Search: Use SearchResults.tsx in Angular template * Search: Render search result header * Search: Move new search components to features/search * Search: Render nested dashboards * Search: Expand dashboard folder * Search: Remove fa prefix from icon names * Search: Enable search results toggling * Search: Add onItemClick handler * Search: Add missing aria-label * Search: Add no results message * Search: Fix e2e selectors * Search: Update SearchField imports * Search: Add conditional classes * Search: Abstract DashboardCheckbox * Search: Separate ResultItem * Search: Style ResultItem * Search: Separate search components * Search: Tweak checkbox styling * Search: Simplify component names * Search: Separate tag component * Search: Checkbox docs * Search: Remove inline on click * Add Tag component * Add Tag story * Add TagList * Group Tab and TabList * Fix typechecks * Remove Meta * Use forwardRef for the Tag * Search: Use TagList from grafana/ui * Search: Add media query for TagList * Search: Add types * Search: Remove selectThemeVariant from SearchItem.tsx * Search: Style section + header * Search: Use semantic html * Search: Adjust section padding * Search: Setup tests * Search: Fix tests * Search: tweak result styles * Search: Expand SearchResults tests * Search: Add SearchItem tests * Search: Use SearchResults in search.html * Search: Toggle search result sections * Search: Make selected prop optional * Search: Fix tag selection * Search: Fix tag filter onChange * Search: Fix uncontrolled state change warning * Search: Update icon names * Search: memoize SearchCheckbox.tsx * Search: Update types * Search: Cleanup events * Search: Semantic html * Use styleMixins * Search: Tweak styling * Search: useCallback for checkbox toggle * Search: Add stylesFactory Co-authored-by: CirceCI <circleci@grafana.com>
356 lines
8.6 KiB
TypeScript
356 lines
8.6 KiB
TypeScript
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';
|
|
import { CoreEvents } from 'app/types';
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
interface SelectedIndicies {
|
|
dashboardIndex?: number;
|
|
folderIndex?: number;
|
|
}
|
|
|
|
interface OpenSearchParams {
|
|
query?: string;
|
|
}
|
|
|
|
export class SearchCtrl {
|
|
isOpen: boolean;
|
|
query: SearchQuery;
|
|
giveSearchFocus: boolean;
|
|
selectedIndex: number;
|
|
results: any;
|
|
currentSearchId: number;
|
|
showImport: boolean;
|
|
dismiss: any;
|
|
ignoreClose: any;
|
|
isLoading: boolean;
|
|
initialFolderFilterTitle: string;
|
|
isEditor: string;
|
|
hasEditPermissionInFolders: boolean;
|
|
queryParser: SearchQueryParser;
|
|
|
|
/** @ngInject */
|
|
constructor(private $scope: any, private $location: any, private $timeout: any, private searchSrv: SearchSrv) {
|
|
appEvents.on(CoreEvents.showDashSearch, this.openSearch.bind(this), $scope);
|
|
appEvents.on(CoreEvents.hideDashSearch, this.closeSearch.bind(this), $scope);
|
|
appEvents.on(CoreEvents.searchQuery, 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(CoreEvents.searchQuery);
|
|
}
|
|
|
|
openSearch(payload: OpenSearchParams = {}) {
|
|
if (this.isOpen) {
|
|
this.closeSearch();
|
|
return;
|
|
}
|
|
|
|
this.isOpen = true;
|
|
this.giveSearchFocus = true;
|
|
this.selectedIndex = -1;
|
|
this.results = [];
|
|
this.query = {
|
|
query: payload.query ? `${payload.query} ` : '',
|
|
parsedQuery: this.queryParser.parse(payload.query),
|
|
tags: [],
|
|
starred: false,
|
|
};
|
|
|
|
this.currentSearchId = 0;
|
|
this.ignoreClose = true;
|
|
this.isLoading = true;
|
|
|
|
this.$timeout(() => {
|
|
this.ignoreClose = false;
|
|
this.giveSearchFocus = true;
|
|
this.search();
|
|
}, 100);
|
|
}
|
|
|
|
onKeyDown(evt: KeyboardEvent) {
|
|
if (evt.keyCode === 27) {
|
|
this.closeSearch();
|
|
}
|
|
if (evt.keyCode === 40) {
|
|
this.moveSelection(1);
|
|
}
|
|
if (evt.keyCode === 38) {
|
|
this.moveSelection(-1);
|
|
}
|
|
if (evt.keyCode === 13) {
|
|
const flattenedResult = this.getFlattenedResultForNavigation();
|
|
const currentItem = flattenedResult[this.selectedIndex];
|
|
|
|
if (currentItem) {
|
|
if (currentItem.dashboardIndex !== undefined) {
|
|
const selectedDash = this.results[currentItem.folderIndex].items[currentItem.dashboardIndex];
|
|
|
|
if (selectedDash) {
|
|
this.$location.search({});
|
|
this.$location.path(selectedDash.url);
|
|
this.closeSearch();
|
|
}
|
|
} else {
|
|
const selectedFolder = this.results[currentItem.folderIndex];
|
|
|
|
if (selectedFolder) {
|
|
selectedFolder.toggle(selectedFolder);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onFilterboxClick() {
|
|
this.giveSearchFocus = false;
|
|
this.preventClose();
|
|
}
|
|
|
|
preventClose() {
|
|
this.ignoreClose = true;
|
|
|
|
this.$timeout(() => {
|
|
this.ignoreClose = false;
|
|
}, 100);
|
|
}
|
|
|
|
moveSelection(direction: number) {
|
|
if (this.results.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const flattenedResult = this.getFlattenedResultForNavigation();
|
|
const currentItem = flattenedResult[this.selectedIndex];
|
|
|
|
if (currentItem) {
|
|
if (currentItem.dashboardIndex !== undefined) {
|
|
this.results[currentItem.folderIndex].items[currentItem.dashboardIndex].selected = false;
|
|
} else {
|
|
this.results[currentItem.folderIndex].selected = false;
|
|
}
|
|
}
|
|
|
|
if (direction === 0) {
|
|
this.selectedIndex = -1;
|
|
return;
|
|
}
|
|
|
|
const max = flattenedResult.length;
|
|
const newIndex = (this.selectedIndex + direction) % max;
|
|
this.selectedIndex = newIndex < 0 ? newIndex + max : newIndex;
|
|
const selectedItem = flattenedResult[this.selectedIndex];
|
|
|
|
if (selectedItem.dashboardIndex === undefined && this.results[selectedItem.folderIndex].id === 0) {
|
|
this.moveSelection(direction);
|
|
return;
|
|
}
|
|
|
|
if (selectedItem.dashboardIndex !== undefined) {
|
|
if (!this.results[selectedItem.folderIndex].expanded) {
|
|
this.moveSelection(direction);
|
|
return;
|
|
}
|
|
|
|
this.results[selectedItem.folderIndex].items[selectedItem.dashboardIndex].selected = true;
|
|
return;
|
|
}
|
|
|
|
if (this.results[selectedItem.folderIndex].hideHeader) {
|
|
this.moveSelection(direction);
|
|
return;
|
|
}
|
|
|
|
this.results[selectedItem.folderIndex].selected = true;
|
|
}
|
|
|
|
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,
|
|
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);
|
|
this.$scope.$digest();
|
|
});
|
|
}
|
|
|
|
queryHasNoFilters() {
|
|
const query = this.query;
|
|
return query.query === '' && query.starred === false && query.tags.length === 0;
|
|
}
|
|
|
|
filterByTag = (tag: string) => {
|
|
if (tag) {
|
|
if (_.indexOf(this.query.tags, tag) === -1) {
|
|
this.query.tags.push(tag);
|
|
this.search();
|
|
}
|
|
}
|
|
};
|
|
|
|
removeTag(tag: string, evt: any) {
|
|
this.query.tags = _.without(this.query.tags, tag);
|
|
this.search();
|
|
this.giveSearchFocus = true;
|
|
evt.stopPropagation();
|
|
evt.preventDefault();
|
|
}
|
|
|
|
getTags = () => {
|
|
return this.searchSrv.getDashboardTags();
|
|
};
|
|
|
|
onTagFiltersChanged = (tags: string[]) => {
|
|
this.query.tags = tags;
|
|
this.search();
|
|
};
|
|
|
|
clearSearchFilter() {
|
|
this.query.query = '';
|
|
this.query.tags = [];
|
|
this.search();
|
|
}
|
|
|
|
showStarred() {
|
|
this.query.starred = !this.query.starred;
|
|
this.giveSearchFocus = true;
|
|
this.search();
|
|
}
|
|
|
|
selectionChanged = () => {
|
|
// TODO remove after React-side state management is implemented
|
|
// This method is only used as a callback after toggling section, to trigger results rerender
|
|
};
|
|
|
|
search() {
|
|
this.showImport = false;
|
|
this.selectedIndex = -1;
|
|
this.searchDashboards(this.query.parsedQuery['folder']);
|
|
}
|
|
|
|
folderExpanding = () => {
|
|
this.moveSelection(0);
|
|
};
|
|
|
|
private getFlattenedResultForNavigation(): SelectedIndicies[] {
|
|
let folderIndex = 0;
|
|
|
|
return _.flatMap(this.results, (s: any) => {
|
|
let result: SelectedIndicies[] = [];
|
|
|
|
result.push({
|
|
folderIndex: folderIndex,
|
|
});
|
|
|
|
let dashboardIndex = 0;
|
|
|
|
result = result.concat(
|
|
_.map(s.items || [], i => {
|
|
return {
|
|
folderIndex: folderIndex,
|
|
dashboardIndex: dashboardIndex++,
|
|
};
|
|
})
|
|
);
|
|
|
|
folderIndex++;
|
|
return result;
|
|
});
|
|
}
|
|
}
|
|
|
|
export function searchDirective() {
|
|
return {
|
|
restrict: 'E',
|
|
templateUrl: 'public/app/core/components/search/search.html',
|
|
controller: SearchCtrl,
|
|
bindToController: true,
|
|
controllerAs: 'ctrl',
|
|
scope: {},
|
|
};
|
|
}
|
|
|
|
coreModule.directive('dashboardSearch', searchDirective);
|