Search: cleanup old Angular files (#23860)

* Search: Remove wrapperRef

* Search: Remove angular search files

* Search: Unify search types

* Search: Remove redundant hideHeader prop

* Search: Remove app/types/search.ts

* Search: Update imports

* Search: Fix type errors
This commit is contained in:
Alex Khomenko 2020-04-24 18:07:57 +03:00 committed by GitHub
parent 58b566a252
commit 1f2a70117b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 43 additions and 2392 deletions

View File

@ -4,7 +4,8 @@ import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { AsyncSelect } from '@grafana/ui';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardSearchHit, DashboardDTO } from 'app/types';
import { DashboardSearchHit } from 'app/features/search/types';
import { DashboardDTO } from 'app/types';
export interface Props {
onSelected: (dashboard: DashboardDTO) => void;

View File

@ -5,7 +5,7 @@ import { debounce } from 'lodash';
import appEvents from '../../app_events';
import { getBackendSrv } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
import { DashboardSearchHit } from '../../../types';
import { DashboardSearchHit } from 'app/features/search/types';
export interface Props {
onChange: ($folder: { title: string; id: number }) => void;

View File

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
const { Select } = LegacyForms;
import { DashboardSearchHit, DashboardSearchHitType } from 'app/types';
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
import { backendSrv } from 'app/core/services/backend_srv';
export interface Props {
@ -50,7 +50,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
id: 0,
title: 'Default',
tags: [],
type: '' as DashboardSearchHitType,
type: '' as DashboardSearchItemType,
uid: '',
uri: '',
url: '',
@ -60,6 +60,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
folderUrl: '',
isStarred: false,
slug: '',
items: [],
};
if (prefs.homeDashboardId > 0 && !dashboards.find(d => d.id === prefs.homeDashboardId)) {

View File

@ -1,118 +0,0 @@
<div class="dashboard-list">
<div
class="page-action-bar page-action-bar--narrow"
ng-hide="ctrl.folderId && !ctrl.hasFilters && ctrl.sections.length === 0"
>
<label class="gf-form gf-form--grow gf-form--has-input-icon">
<input
type="text"
class="gf-form-input max-width-30"
placeholder="Search dashboards by name"
tabindex="1"
give-focus="true"
ng-model="ctrl.query.query"
ng-model-options="{ debounce: 500 }"
spellcheck="false"
ng-change="ctrl.onQueryChange()"
/>
<icon class="gf-form-input-icon" name="'search'" style="margin-top: -5px"></icon>
</label>
<div class="page-action-bar__spacer"></div>
<a
class="btn btn-primary"
ng-href="{{ctrl.createDashboardUrl()}}"
ng-if="ctrl.hasEditPermissionInFolders || ctrl.canSave"
>
New Dashboard
</a>
<a class="btn btn-primary" href="dashboards/folder/new" ng-if="!ctrl.folderId && ctrl.isEditor">
New Folder
</a>
<a
class="btn btn-primary"
href="{{ctrl.importDashboardUrl()}}"
ng-if="ctrl.hasEditPermissionInFolders || ctrl.canSave"
>
Import
</a>
</div>
<div class="page-action-bar page-action-bar--narrow" ng-show="ctrl.hasFilters">
<div class="gf-form-inline">
<div class="gf-form" ng-show="ctrl.query.tag.length > 0">
<label class="gf-form-label width-4">
Tags
</label>
<div class="gf-form-input gf-form-input--plaintext" ng-show="ctrl.query.tag.length > 0">
<span ng-repeat="tagName in ctrl.query.tag">
<a ng-click="ctrl.removeTag(tagName, $event)" tag-color-from-name="tagName" class="tag label label-tag">
<icon name="'times'"></icon>&nbsp;{{tagName}}
</a>
</span>
</div>
</div>
<div class="gf-form" ng-show="ctrl.query.starred">
<label class="gf-form-label">
<a class="pointer" ng-click="ctrl.removeStarred()"> <icon name="'check'"></icon> Starred </a>
</label>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" ng-click="ctrl.clearFilters()" bs-tooltip="'Clear current search query and filters'">
<icon name="'times'"></icon>&nbsp;Clear
</a>
</label>
</div>
</div>
</div>
<div class="search-results" ng-show="ctrl.hasFilters && ctrl.sections.length === 0">
<em class="muted">
No dashboards matching your search were found.
</em>
</div>
<div class="search-results" ng-show="!ctrl.folderId && !ctrl.hasFilters && ctrl.sections.length === 0">
<em class="muted">
No dashboards found.
</em>
</div>
<div class="search-results" ng-show="ctrl.sections.length > 0">
<search-filters
on-select-all-changed="ctrl.onSelectAllChanged"
all-checked="ctrl.selectAllChecked"
can-move="ctrl.canMove"
can-delete="ctrl.canDelete"
move-to="ctrl.moveTo"
delete-item="ctrl.delete"
tag-filter-options="ctrl.tagFilterOptions"
selected-starred-filter="ctrl.selectedStarredFilter"
on-starred-filter-change="ctrl.onStarredFilterChange"
selected-tag-filter="ctrl.selectedTagFilter"
on-tagfilter-change="ctrl.onTagFilterChange"
/>
<div class="search-results-container">
<search-results
results="ctrl.sections"
editable="true"
on-selection-changed="ctrl.selectionChanged"
on-tag-selected="ctrl.filterByTag"
on-toggle-selection="ctrl.toggleSelection"
/>
</div>
</div>
</div>
<div ng-if="ctrl.canSave && ctrl.folderId && !ctrl.hasFilters && ctrl.sections.length === 0">
<empty-list-cta
title="'This folder doesn\'t have any dashboards yet'"
buttonIcon="'plus'"
buttonLink="'dashboard/new?folderId={{ctrl.folderId}}'"
buttonTitle="'Create Dashboard'"
proTip="'Add/move dashboards to your folder at ->'"
proTipLink="'dashboards'"
proTipLinkTitle="'Manage dashboards'"
proTipTarget=""
/>
</div>

View File

@ -1,357 +0,0 @@
import { IScope } from 'angular';
import _ from 'lodash';
import { SelectableValue } from '@grafana/data';
//@ts-ignore
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { SearchSrv } from 'app/core/services/search_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import { ContextSrv } from 'app/core/services/context_srv';
import { CoreEvents } from 'app/types';
import { promiseToDigest } from '../../utils/promiseToDigest';
export interface Section {
id: number;
uid: string;
title: string;
expanded: boolean;
removable: boolean;
items: any[];
url: string;
icon: string;
score: number;
checked: boolean;
hideHeader: boolean;
toggle: Function;
type?: string;
}
export interface FoldersAndDashboardUids {
folderUids: string[];
dashboardUids: string[];
}
class Query {
query: string;
mode: string;
tag: any[];
starred: boolean;
skipRecent: boolean;
skipStarred: boolean;
folderIds: number[];
}
export class ManageDashboardsCtrl {
sections: Section[];
query: Query;
navModel: any;
selectAllChecked = false;
// enable/disable actions depending on the folders or dashboards selected
canDelete = false;
canMove = false;
// filter variables
hasFilters = false;
tagFilterOptions: any[];
selectedTagFilter: any;
selectedStarredFilter: any;
// used when managing dashboards for a specific folder
folderId?: number;
folderUid?: string;
// if user can add new folders and/or add new dashboards
canSave = false;
// if user has editor role or higher
isEditor: boolean;
hasEditPermissionInFolders: boolean;
/** @ngInject */
constructor(private $scope: IScope, private searchSrv: SearchSrv, private contextSrv: ContextSrv) {
this.isEditor = this.contextSrv.isEditor;
this.hasEditPermissionInFolders = this.contextSrv.hasEditPermissionInFolders;
this.query = {
query: '',
mode: 'tree',
tag: [],
starred: false,
skipRecent: true,
skipStarred: true,
folderIds: [],
};
if (this.folderId) {
this.query.folderIds = [this.folderId];
}
this.refreshList().then(() => {
this.initTagFilter();
});
}
refreshList() {
return this.searchSrv
.search(this.query)
.then((result: Section[]) => {
return this.initDashboardList(result);
})
.then(() => {
if (!this.folderUid) {
this.$scope.$digest();
return undefined;
}
return backendSrv.getFolderByUid(this.folderUid).then((folder: any) => {
this.canSave = folder.canSave;
if (!this.canSave) {
this.hasEditPermissionInFolders = false;
}
this.$scope.$digest();
});
});
}
initDashboardList(result: Section[]) {
this.canMove = false;
this.canDelete = false;
this.selectAllChecked = false;
this.hasFilters = this.query.query.length > 0 || this.query.tag.length > 0 || this.query.starred;
if (!result) {
this.sections = [];
return;
}
this.sections = result;
for (const section of this.sections) {
section.checked = false;
for (const dashboard of section.items) {
dashboard.checked = false;
}
}
if (this.folderId && this.sections.length > 0) {
this.sections[0].hideHeader = true;
}
}
selectionChanged = () => {
let selectedDashboards = 0;
if (this.sections) {
for (const section of this.sections) {
selectedDashboards += _.filter(section.items, { checked: true } as any).length;
}
const selectedFolders = _.filter(this.sections, { checked: true }).length;
this.canMove = selectedDashboards > 0;
this.canDelete = selectedDashboards > 0 || selectedFolders > 0;
}
};
getFoldersAndDashboardsToDelete(): FoldersAndDashboardUids {
const selectedDashboards: FoldersAndDashboardUids = {
folderUids: [],
dashboardUids: [],
};
for (const section of this.sections) {
if (section.checked && section.id !== 0) {
selectedDashboards.folderUids.push(section.uid);
} else {
const selected = _.filter(section.items, { checked: true } as any);
selectedDashboards.dashboardUids.push(..._.map(selected, 'uid'));
}
}
return selectedDashboards;
}
getFolderIds(sections: Section[]) {
const ids = [];
for (const s of sections) {
if (s.checked) {
ids.push(s.id);
}
}
return ids;
}
delete = () => {
const data = this.getFoldersAndDashboardsToDelete();
const folderCount = data.folderUids.length;
const dashCount = data.dashboardUids.length;
let text = 'Do you want to delete the ';
let text2;
if (folderCount > 0 && dashCount > 0) {
text += `selected folder${folderCount === 1 ? '' : 's'} and dashboard${dashCount === 1 ? '' : 's'}?`;
text2 = `All dashboards of the selected folder${folderCount === 1 ? '' : 's'} will also be deleted`;
} else if (folderCount > 0) {
text += `selected folder${folderCount === 1 ? '' : 's'} and all its dashboards?`;
} else {
text += `selected dashboard${dashCount === 1 ? '' : 's'}?`;
}
appEvents.emit(CoreEvents.showConfirmModal, {
title: 'Delete',
text: text,
text2: text2,
icon: 'trash-alt',
yesText: 'Delete',
onConfirm: () => {
this.deleteFoldersAndDashboards(data.folderUids, data.dashboardUids);
},
});
};
private deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) {
promiseToDigest(this.$scope)(
backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => {
this.refreshList();
})
);
}
getDashboardsToMove() {
const selectedDashboards = [];
for (const section of this.sections) {
const selected = _.filter(section.items, { checked: true } as any);
selectedDashboards.push(..._.map(selected, 'uid'));
}
return selectedDashboards;
}
moveTo = () => {
const selectedDashboards = this.getDashboardsToMove();
const template =
'<move-to-folder-modal dismiss="dismiss()" ' +
'dashboards="model.dashboards" after-save="model.afterSave()">' +
'</move-to-folder-modal>';
appEvents.emit(CoreEvents.showModal, {
templateHtml: template,
modalClass: 'modal--narrow',
model: {
dashboards: selectedDashboards,
afterSave: this.refreshList.bind(this),
},
});
};
initTagFilter() {
return this.searchSrv.getDashboardTags().then((results: any) => {
this.tagFilterOptions = results.map((result: any) => ({ value: result.term, label: result.term }));
});
}
filterByTag = (tag: any) => {
if (tag) {
if (_.indexOf(this.query.tag, tag) === -1) {
this.query.tag.push(tag);
}
}
return this.refreshList();
};
onQueryChange() {
return this.refreshList();
}
onTagFilterChange = (filter: SelectableValue) => {
const res = this.filterByTag(filter.value);
this.selectedTagFilter = filter.value;
return res;
};
removeTag(tag: any, evt: Event) {
this.query.tag = _.without(this.query.tag, tag);
this.refreshList();
if (evt) {
evt.stopPropagation();
evt.preventDefault();
}
}
removeStarred() {
this.query.starred = false;
return this.refreshList();
}
onStarredFilterChange = (filter: SelectableValue) => {
this.query.starred = filter.value;
this.selectedStarredFilter = filter.value;
return this.refreshList();
};
onSelectAllChanged = () => {
this.selectAllChecked = !this.selectAllChecked;
for (const section of this.sections) {
if (!section.hideHeader) {
section.checked = this.selectAllChecked;
}
section.items = _.map(section.items, (item: any) => {
item.checked = this.selectAllChecked;
return item;
});
}
this.selectionChanged();
};
clearFilters() {
this.query.query = '';
this.query.tag = [];
this.query.starred = false;
this.selectedStarredFilter = 'starred';
this.selectedTagFilter = 'tag';
this.refreshList();
}
createDashboardUrl() {
let url = 'dashboard/new';
if (this.folderId) {
url += `?folderId=${this.folderId}`;
}
return url;
}
importDashboardUrl() {
let url = 'dashboard/import';
if (this.folderId) {
url += `?folderId=${this.folderId}`;
}
return url;
}
}
export function manageDashboardsDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/core/components/manage_dashboards/manage_dashboards.html',
controller: ManageDashboardsCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
folderId: '=',
folderUid: '=',
},
};
}
//coreModule.directive('manageDashboards', manageDashboardsDirective);

View File

@ -1,62 +0,0 @@
<div class="search-backdrop" ng-if="ctrl.isOpen"></div>
<div class="search-container" ng-if="ctrl.isOpen">
<search-field
query="ctrl.query"
autoFocus="ctrl.giveSearchFocus"
on-change="ctrl.onQueryChange"
on-key-down="ctrl.onKeyDown"
/>
<div class="search-dropdown">
<div class="search-dropdown__col_1">
<div class="search-results-scroller">
<div class="search-results-container" grafana-scrollbar>
<search-results
results="ctrl.results"
on-tag-selected="ctrl.filterByTag"
on-folder-expanding="ctrl.folderExpanding"
on-selection-changed="ctrl.selectionChanged"
/>
</div>
</div>
</div>
<div class="search-dropdown__col_2">
<div class="search-filter-box" ng-click="ctrl.onFilterboxClick()">
<div class="search-filter-box__header">
<icon name="'filter'"></icon>
Filter by:
<a class="pointer pull-right small" ng-click="ctrl.clearSearchFilter()">
<icon name="'times'" size="'sm'"></icon> Clear
</a>
</div>
<tag-filter tags="ctrl.query.tags" tagOptions="ctrl.getTags" on-change="ctrl.onTagFiltersChanged"> </tag-filter>
</div>
<div class="search-filter-box" ng-if="ctrl.isEditor || ctrl.hasEditPermissionInFolders">
<a href="dashboard/new" class="search-filter-box-link">
<icon name="'plus-square'" size="'xl'" style="margin-right: 8px;"></icon> New dashboard
</a>
<a href="dashboards/folder/new" class="search-filter-box-link" ng-if="ctrl.isEditor">
<icon name="'folder-plus'" size="'xl'" style="margin-right: 8px;"></icon> New folder
</a>
<a
href="dashboard/import"
class="search-filter-box-link"
ng-if="ctrl.isEditor || ctrl.hasEditPermissionInFolders"
>
<icon name="'import'" size="'xl'" style="margin-right: 8px;"></icon> Import dashboard
</a>
<a
class="search-filter-box-link"
target="_blank"
href="https://grafana.com/dashboards?utm_source=grafana_search"
>
<icon name="'apps'" size="'xl'" style="margin-right: 8px;"></icon> Find dashboards on Grafana.com
</a>
</div>
</div>
</div>
</div>

View File

@ -1,355 +0,0 @@
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);

View File

@ -1,64 +0,0 @@
<div ng-repeat="section in ctrl.results" class="search-section">
<div
class="search-section__header pointer"
ng-hide="section.hideHeader"
ng-class="{'selected': section.selected}"
ng-click="ctrl.toggleFolderExpand(section)"
>
<div ng-click="ctrl.toggleSelection(section, $event)" class="center-vh">
<gf-form-checkbox
ng-show="ctrl.editable"
on-change="ctrl.selectionChanged($event)"
checked="section.checked"
switch-class="gf-form-checkbox--transparent"
>
</gf-form-checkbox>
</div>
<icon class="search-section__header__icon" ng-class="section.icon"></icon>
<span class="search-section__header__text">{{::section.title}}</span>
<a ng-show="section.url" href="{{section.url}}" class="search-section__header__link">
<icon name="'cog'"></icon>
</a>
<icon class="search-section__header__toggle" name="'angle-down'" ng-show="section.expanded"></icon>
<icon class="search-section__header__toggle" name="'angle-right'" ng-hide="section.expanded"></icon>
</div>
<div class="search-section__header" ng-show="section.hideHeader"></div>
<div ng-if="section.expanded">
<a
ng-repeat="item in section.items"
class="search-item search-item--indent"
ng-class="{'selected': item.selected}"
ng-href="{{::item.url}}"
aria-label="{{ctrl.selectors.dashboards(item.title)}}"
>
<div ng-click="ctrl.toggleSelection(item, $event)" class="center-vh">
<gf-form-checkbox
ng-show="ctrl.editable"
on-change="ctrl.selectionChanged()"
checked="item.checked"
switch-class="gf-form-checkbox--transparent"
>
</gf-form-checkbox>
</div>
<span class="search-item__icon">
<i class="gicon mini gicon-dashboard-list"></i>
</span>
<span class="search-item__body" ng-click="ctrl.onItemClick(item)">
<div class="search-item__body-title">{{::item.title}}</div>
<span class="search-item__body-folder-title">{{::item.folderTitle}}</span>
</span>
<span class="search-item__tags">
<span
ng-click="ctrl.selectTag(tag, $event)"
ng-repeat="tag in item.tags"
tag-color-from-name="tag"
class="label label-tag"
>
{{tag}}
</span>
</span>
</a>
</div>
</div>

View File

@ -1,102 +0,0 @@
import _ from 'lodash';
import { ILocationService, IScope } from 'angular';
import { e2e } from '@grafana/e2e';
import coreModule from '../../core_module';
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
import { promiseToDigest } from '../../utils/promiseToDigest';
export class SearchResultsCtrl {
results: any;
onSelectionChanged: any;
onTagSelected: any;
onFolderExpanding: any;
editable: boolean;
selectors: typeof e2e.pages.Dashboards.selectors;
/** @ngInject */
constructor(private $location: ILocationService, private $scope: IScope) {
this.selectors = e2e.pages.Dashboards.selectors;
}
toggleFolderExpand(section: any) {
if (section.toggle) {
if (!section.expanded && this.onFolderExpanding) {
this.onFolderExpanding();
}
promiseToDigest(this.$scope)(
section.toggle(section).then((f: any) => {
if (this.editable && f.expanded) {
if (f.items) {
_.each(f.items, i => {
i.checked = f.checked;
});
if (this.onSelectionChanged) {
this.onSelectionChanged();
}
}
}
})
);
}
}
toggleSelection(item: any, evt: any) {
item.checked = !item.checked;
if (item.items) {
_.each(item.items, i => {
i.checked = item.checked;
});
}
if (this.onSelectionChanged) {
this.onSelectionChanged();
}
if (evt) {
evt.stopPropagation();
evt.preventDefault();
}
}
onItemClick(item: any) {
//Check if one string can be found in the other
if (this.$location.path().indexOf(item.url) > -1 || item.url.indexOf(this.$location.path()) > -1) {
appEvents.emit(CoreEvents.hideDashSearch);
}
}
selectTag(tag: any, evt: any) {
if (this.onTagSelected) {
this.onTagSelected({ $tag: tag });
}
if (evt) {
evt.stopPropagation();
evt.preventDefault();
}
}
}
export function searchResultsDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/core/components/search/search_results.html',
controller: SearchResultsCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
editable: '@',
results: '=',
onSelectionChanged: '&',
onTagSelected: '&',
onFolderExpanding: '&',
},
};
}
coreModule.directive('dashboardSearchResults', searchResultsDirective);

View File

@ -18,7 +18,6 @@ import './services/search_srv';
import './services/ng_react';
import { colors, JsonExplorer } from '@grafana/ui/';
import { searchDirective } from './components/search/search';
import { infoPopover } from './components/info_popover';
import { arrayJoin } from './directives/array_join';
import { liveSrv } from './live/live_srv';
@ -42,8 +41,6 @@ import { profiler } from './profiler';
import { registerAngularDirectives } from './angular_wrappers';
import { updateLegendValues } from './time_series2';
import TimeSeries from './time_series2';
import { searchResultsDirective } from './components/search/search_results';
import { manageDashboardsDirective } from './components/manage_dashboards/manage_dashboards';
import { NavModel } from '@grafana/data';
export {
@ -51,7 +48,6 @@ export {
registerAngularDirectives,
arrayJoin,
coreModule,
searchDirective,
liveSrv,
switchDirective,
infoPopover,
@ -69,8 +65,6 @@ export {
NavModelSrv,
NavModel,
geminiScrollbar,
manageDashboardsDirective,
TimeSeries,
updateLegendValues,
searchResultsDirective,
};

View File

@ -7,12 +7,12 @@ import { AppEvents } from '@grafana/data';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { DashboardSearchHit } from 'app/types/search';
import { DataSourceResponse } from 'app/types/events';
import { DashboardSearchHit } from 'app/features/search/types';
import { CoreEvents, DashboardDTO, FolderInfo } from 'app/types';
import { ContextSrv, contextSrv } from './context_srv';
import { coreModule } from 'app/core/core_module';
import { ContextSrv, contextSrv } from './context_srv';
import { Emitter } from '../utils/emitter';
import { DataSourceResponse } from '../../types/events';
import { parseInitFromOptions, parseUrlFromOptions } from '../utils/fetch';
export interface DatasourceRequestOptions {

View File

@ -1,16 +1,14 @@
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import impressionSrv from 'app/core/services/impression_srv';
import store from 'app/core/store';
import { contextSrv } from 'app/core/services/context_srv';
import { hasFilters } from 'app/features/search/utils';
import { DashboardSection, DashboardSearchItemType, DashboardSearchHit } from 'app/features/search/types';
import { backendSrv } from './backend_srv';
import { Section } from '../components/manage_dashboards/manage_dashboards';
import { DashboardSearchHit, DashboardSearchHitType } from 'app/types/search';
import { hasFilters } from '../../features/search/utils';
interface Sections {
[key: string]: Partial<Section>;
[key: string]: Partial<DashboardSection>;
}
export class SearchSrv {
@ -22,18 +20,16 @@ export class SearchSrv {
this.starredIsOpen = store.getBool('search.sections.starred', true);
}
private getRecentDashboards(sections: Sections) {
private getRecentDashboards(sections: DashboardSection[] | any) {
return this.queryForRecentDashboards().then((result: any[]) => {
if (result.length > 0) {
sections['recent'] = {
title: 'Recent',
icon: 'clock-nine',
score: -1,
removable: true,
expanded: this.recentIsOpen,
toggle: this.toggleRecent.bind(this),
items: result,
type: DashboardSearchHitType.DashHitFolder,
type: DashboardSearchItemType.DashFolder,
};
}
});
@ -50,45 +46,24 @@ export class SearchSrv {
.map(orderId => {
return _.find(result, { id: orderId });
})
.filter(hit => hit && !hit.isStarred);
.filter(hit => hit && !hit.isStarred) as DashboardSearchHit[];
});
}
private toggleRecent(section: Section) {
this.recentIsOpen = section.expanded = !section.expanded;
store.set('search.sections.recent', this.recentIsOpen);
if (!section.expanded || section.items.length) {
return Promise.resolve(section);
}
return this.queryForRecentDashboards().then(result => {
section.items = result;
return Promise.resolve(section);
});
}
private toggleStarred(section: Section) {
this.starredIsOpen = section.expanded = !section.expanded;
store.set('search.sections.starred', this.starredIsOpen);
return Promise.resolve(section);
}
private getStarred(sections: Sections) {
private getStarred(sections: DashboardSection) {
if (!contextSrv.isSignedIn) {
return Promise.resolve();
}
return backendSrv.search({ starred: true, limit: 30 }).then(result => {
if (result.length > 0) {
sections['starred'] = {
(sections as any)['starred'] = {
title: 'Starred',
icon: 'star',
score: -2,
expanded: this.starredIsOpen,
toggle: this.toggleStarred.bind(this),
items: result,
type: DashboardSearchHitType.DashHitFolder,
type: DashboardSearchItemType.DashFolder,
};
}
});
@ -138,7 +113,6 @@ export class SearchSrv {
title: hit.title,
expanded: false,
items: [],
toggle: this.toggleFolder.bind(this),
url: hit.url,
icon: 'folder',
score: _.keys(sections).length,
@ -162,9 +136,8 @@ export class SearchSrv {
url: hit.folderUrl,
items: [],
icon: 'folder-open',
toggle: this.toggleFolder.bind(this),
score: _.keys(sections).length,
type: DashboardSearchHitType.DashHitFolder,
type: DashboardSearchItemType.DashFolder,
};
} else {
section = {
@ -172,9 +145,8 @@ export class SearchSrv {
title: 'General',
items: [],
icon: 'folder-open',
toggle: this.toggleFolder.bind(this),
score: _.keys(sections).length,
type: DashboardSearchHitType.DashHitFolder,
type: DashboardSearchItemType.DashFolder,
};
}
// add section
@ -182,28 +154,10 @@ export class SearchSrv {
}
section.expanded = true;
section.items.push(hit);
section.items && section.items.push(hit);
}
}
private toggleFolder(section: Section) {
section.expanded = !section.expanded;
section.icon = section.expanded ? 'folder-open' : 'folder';
if (section.items.length) {
return Promise.resolve(section);
}
const query = {
folderIds: [section.id],
};
return backendSrv.search(query).then(results => {
section.items = results;
return Promise.resolve(section);
});
}
getDashboardTags() {
return backendSrv.get('/api/dashboards/tags');
}
@ -212,5 +166,3 @@ export class SearchSrv {
return backendSrv.get('/api/search/sorting');
}
}
coreModule.service('searchSrv', SearchSrv);

View File

@ -1,598 +0,0 @@
// @ts-ignore
import q from 'q';
import {
ManageDashboardsCtrl,
Section,
FoldersAndDashboardUids,
} from 'app/core/components/manage_dashboards/manage_dashboards';
import { SearchSrv } from 'app/core/services/search_srv';
import { ContextSrv } from '../services/context_srv';
const mockSection = (overides?: object): Section => {
const defaultSection: Section = {
id: 0,
items: [],
checked: false,
expanded: false,
removable: false,
hideHeader: false,
icon: '',
score: 0,
title: 'Some Section',
toggle: jest.fn(),
uid: 'someuid',
url: '/some/url/',
};
return { ...defaultSection, ...overides };
};
describe('ManageDashboards', () => {
let ctrl: ManageDashboardsCtrl;
describe('when browsing dashboards', () => {
beforeEach(() => {
const tags: any[] = [];
const response = [
{
id: 410,
title: 'afolder',
type: 'dash-folder',
items: [
{
id: 399,
title: 'Dashboard Test',
url: 'dashboard/db/dashboard-test',
icon: 'folder',
tags,
isStarred: false,
},
],
tags,
isStarred: false,
},
{
id: 0,
title: 'General',
icon: 'folder-open',
uri: 'db/something-else',
type: 'dash-db',
items: [
{
id: 500,
title: 'Dashboard Test',
url: 'dashboard/db/dashboard-test',
icon: 'folder',
tags,
isStarred: false,
},
],
tags,
isStarred: false,
},
];
ctrl = createCtrlWithStubs(response);
return ctrl.refreshList();
});
it('should set checked to false on all sections and children', () => {
expect(ctrl.sections.length).toEqual(2);
expect(ctrl.sections[0].checked).toEqual(false);
expect(ctrl.sections[0].items[0].checked).toEqual(false);
expect(ctrl.sections[1].checked).toEqual(false);
expect(ctrl.sections[1].items[0].checked).toEqual(false);
expect(ctrl.sections[0].hideHeader).toBeFalsy();
});
});
describe('when browsing dashboards for a folder', () => {
beforeEach(() => {
const tags: any[] = [];
const response = [
{
id: 410,
title: 'afolder',
type: 'dash-folder',
items: [
{
id: 399,
title: 'Dashboard Test',
url: 'dashboard/db/dashboard-test',
icon: 'folder',
tags,
isStarred: false,
},
],
tags,
isStarred: false,
},
];
ctrl = createCtrlWithStubs(response);
ctrl.folderId = 410;
return ctrl.refreshList();
});
it('should set hide header to true on section', () => {
expect(ctrl.sections[0].hideHeader).toBeTruthy();
});
});
describe('when searching dashboards', () => {
beforeEach(() => {
const tags: any[] = [];
const response = [
{
checked: false,
expanded: true,
hideHeader: true,
items: [
{
id: 399,
title: 'Dashboard Test',
url: 'dashboard/db/dashboard-test',
icon: 'folder',
tags,
isStarred: false,
folderId: 410,
folderUid: 'uid',
folderTitle: 'Folder',
folderUrl: '/dashboards/f/uid/folder',
},
{
id: 500,
title: 'Dashboard Test',
url: 'dashboard/db/dashboard-test',
icon: 'folder',
tags,
folderId: 499,
isStarred: false,
},
],
},
];
ctrl = createCtrlWithStubs(response);
});
describe('with query filter', () => {
beforeEach(() => {
ctrl.query.query = 'd';
ctrl.canMove = true;
ctrl.canDelete = true;
ctrl.selectAllChecked = true;
return ctrl.refreshList();
});
it('should set checked to false on all sections and children', () => {
expect(ctrl.sections.length).toEqual(1);
expect(ctrl.sections[0].checked).toEqual(false);
expect(ctrl.sections[0].items[0].checked).toEqual(false);
expect(ctrl.sections[0].items[1].checked).toEqual(false);
});
it('should uncheck select all', () => {
expect(ctrl.selectAllChecked).toBeFalsy();
});
it('should disable Move To button', () => {
expect(ctrl.canMove).toBeFalsy();
});
it('should disable delete button', () => {
expect(ctrl.canDelete).toBeFalsy();
});
it('should have active filters', () => {
expect(ctrl.hasFilters).toBeTruthy();
});
describe('when select all is checked', () => {
beforeEach(() => {
ctrl.selectAllChecked = false;
ctrl.onSelectAllChanged();
});
it('should select all dashboards', () => {
expect(ctrl.sections[0].checked).toBeFalsy();
expect(ctrl.sections[0].items[0].checked).toBeTruthy();
expect(ctrl.sections[0].items[1].checked).toBeTruthy();
});
it('should enable Move To button', () => {
expect(ctrl.canMove).toBeTruthy();
});
it('should enable delete button', () => {
expect(ctrl.canDelete).toBeTruthy();
});
describe('when clearing filters', () => {
beforeEach(() => {
return ctrl.clearFilters();
});
it('should reset query filter', () => {
expect(ctrl.query.query).toEqual('');
});
});
});
});
describe('with tag filter', () => {
beforeEach(() => {
return ctrl.filterByTag('test');
});
it('should set tag filter', () => {
expect(ctrl.sections.length).toEqual(1);
expect(ctrl.query.tag[0]).toEqual('test');
});
it('should have active filters', () => {
expect(ctrl.hasFilters).toBeTruthy();
});
describe('when clearing filters', () => {
beforeEach(() => {
return ctrl.clearFilters();
});
it('should reset tag filter', () => {
expect(ctrl.query.tag.length).toEqual(0);
});
});
});
describe('with starred filter', () => {
beforeEach(() => {
const yesOption: any = { label: 'Yes', value: true };
ctrl.selectedStarredFilter = yesOption;
return ctrl.onStarredFilterChange(yesOption);
});
it('should set starred filter', () => {
expect(ctrl.sections.length).toEqual(1);
expect(ctrl.query.starred).toEqual(true);
});
it('should have active filters', () => {
expect(ctrl.hasFilters).toBeTruthy();
});
describe('when clearing filters', () => {
beforeEach(() => {
return ctrl.clearFilters();
});
it('should reset starred filter', () => {
expect(ctrl.query.starred).toEqual(false);
});
});
});
});
describe('when selecting dashboards', () => {
let ctrl: ManageDashboardsCtrl;
beforeEach(() => {
ctrl = createCtrlWithStubs([]);
});
describe('and no dashboards are selected', () => {
beforeEach(() => {
ctrl.sections = [
mockSection({
id: 1,
items: [{ id: 2, checked: false }],
checked: false,
}),
mockSection({
id: 0,
items: [{ id: 3, checked: false }],
checked: false,
}),
];
ctrl.selectionChanged();
});
it('should disable Move To button', () => {
expect(ctrl.canMove).toBeFalsy();
});
it('should disable delete button', () => {
expect(ctrl.canDelete).toBeFalsy();
});
describe('when select all is checked', () => {
beforeEach(() => {
ctrl.selectAllChecked = false;
ctrl.onSelectAllChanged();
});
it('should select all folders and dashboards', () => {
expect(ctrl.sections[0].checked).toBeTruthy();
expect(ctrl.sections[0].items[0].checked).toBeTruthy();
expect(ctrl.sections[1].checked).toBeTruthy();
expect(ctrl.sections[1].items[0].checked).toBeTruthy();
});
it('should enable Move To button', () => {
expect(ctrl.canMove).toBeTruthy();
});
it('should enable delete button', () => {
expect(ctrl.canDelete).toBeTruthy();
});
});
});
describe('and all folders and dashboards are selected', () => {
beforeEach(() => {
ctrl.sections = [
mockSection({
id: 1,
items: [{ id: 2, checked: true }],
checked: true,
}),
mockSection({
id: 0,
items: [{ id: 3, checked: true }],
checked: true,
}),
];
ctrl.selectionChanged();
});
it('should enable Move To button', () => {
expect(ctrl.canMove).toBeTruthy();
});
it('should enable delete button', () => {
expect(ctrl.canDelete).toBeTruthy();
});
describe('when select all is unchecked', () => {
beforeEach(() => {
ctrl.selectAllChecked = true;
ctrl.onSelectAllChanged();
});
it('should uncheck all checked folders and dashboards', () => {
expect(ctrl.sections[0].checked).toBeFalsy();
expect(ctrl.sections[0].items[0].checked).toBeFalsy();
expect(ctrl.sections[1].checked).toBeFalsy();
expect(ctrl.sections[1].items[0].checked).toBeFalsy();
});
it('should disable Move To button', () => {
expect(ctrl.canMove).toBeFalsy();
});
it('should disable delete button', () => {
expect(ctrl.canDelete).toBeFalsy();
});
});
});
describe('and one dashboard in root is selected', () => {
beforeEach(() => {
ctrl.sections = [
mockSection({
id: 1,
title: 'folder',
items: [{ id: 2, checked: false }],
checked: false,
}),
mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: true }],
checked: false,
}),
];
ctrl.selectionChanged();
});
it('should enable Move To button', () => {
expect(ctrl.canMove).toBeTruthy();
});
it('should enable delete button', () => {
expect(ctrl.canDelete).toBeTruthy();
});
});
describe('and one child dashboard is selected', () => {
beforeEach(() => {
ctrl.sections = [
mockSection({
id: 1,
title: 'folder',
items: [{ id: 2, checked: true }],
checked: false,
}),
mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: false }],
checked: false,
}),
];
ctrl.selectionChanged();
});
it('should enable Move To button', () => {
expect(ctrl.canMove).toBeTruthy();
});
it('should enable delete button', () => {
expect(ctrl.canDelete).toBeTruthy();
});
});
describe('and one child dashboard and one dashboard is selected', () => {
beforeEach(() => {
ctrl.sections = [
mockSection({
id: 1,
title: 'folder',
items: [{ id: 2, checked: true }],
checked: false,
}),
mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: true }],
checked: false,
}),
];
ctrl.selectionChanged();
});
it('should enable Move To button', () => {
expect(ctrl.canMove).toBeTruthy();
});
it('should enable delete button', () => {
expect(ctrl.canDelete).toBeTruthy();
});
});
describe('and one child dashboard and one folder is selected', () => {
beforeEach(() => {
ctrl.sections = [
mockSection({
id: 1,
title: 'folder',
items: [{ id: 2, checked: false }],
checked: true,
}),
mockSection({
id: 3,
title: 'folder',
items: [{ id: 4, checked: true }],
checked: false,
}),
mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: false }],
checked: false,
}),
];
ctrl.selectionChanged();
});
it('should enable Move To button', () => {
expect(ctrl.canMove).toBeTruthy();
});
it('should enable delete button', () => {
expect(ctrl.canDelete).toBeTruthy();
});
});
});
describe('when deleting dashboards', () => {
let toBeDeleted: FoldersAndDashboardUids;
beforeEach(() => {
ctrl = createCtrlWithStubs([]);
ctrl.sections = [
mockSection({
id: 1,
uid: 'folder',
title: 'folder',
items: [{ id: 2, checked: true, uid: 'folder-dash' }],
checked: true,
}),
mockSection({
id: 3,
title: 'folder-2',
items: [{ id: 3, checked: true, uid: 'folder-2-dash' }],
checked: false,
uid: 'folder-2',
}),
mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: true, uid: 'root-dash' }],
checked: true,
}),
];
toBeDeleted = ctrl.getFoldersAndDashboardsToDelete();
});
it('should return 1 folder', () => {
expect(toBeDeleted.folderUids.length).toEqual(1);
});
it('should return 2 dashboards', () => {
expect(toBeDeleted.dashboardUids.length).toEqual(2);
});
it('should filter out children if parent is checked', () => {
expect(toBeDeleted.folderUids[0]).toEqual('folder');
});
it('should not filter out children if parent not is checked', () => {
expect(toBeDeleted.dashboardUids[0]).toEqual('folder-2-dash');
});
it('should not filter out children if parent is checked and root', () => {
expect(toBeDeleted.dashboardUids[1]).toEqual('root-dash');
});
});
describe('when moving dashboards', () => {
beforeEach(() => {
ctrl = createCtrlWithStubs([]);
ctrl.sections = [
mockSection({
id: 1,
title: 'folder',
items: [{ id: 2, checked: true, uid: 'dash' }],
checked: false,
uid: 'folder',
}),
mockSection({
id: 0,
title: 'General',
items: [{ id: 3, checked: true, uid: 'dash-2' }],
checked: false,
}),
];
});
it('should get selected dashboards', () => {
const toBeMove = ctrl.getDashboardsToMove();
expect(toBeMove.length).toEqual(2);
expect(toBeMove[0]).toEqual('dash');
expect(toBeMove[1]).toEqual('dash-2');
});
});
});
function createCtrlWithStubs(searchResponse: any, tags?: any) {
const searchSrvStub = {
search: (options: any) => {
return q.resolve(searchResponse);
},
getDashboardTags: () => {
return q.resolve(tags || []);
},
};
return new ManageDashboardsCtrl(
{ $digest: jest.fn() } as any,
searchSrvStub as SearchSrv,
{ isEditor: true } as ContextSrv
);
}

View File

@ -1,341 +0,0 @@
import { SearchCtrl } from '../components/search/search';
import { SearchSrv } from '../services/search_srv';
jest.mock('app/core/services/context_srv', () => ({
contextSrv: {
user: { orgId: 1 },
},
}));
describe('SearchCtrl', () => {
const searchSrvStub = {
search: (options: any) => {},
getDashboardTags: () => {},
};
const ctrl = new SearchCtrl({ $on: () => {} }, {}, {}, searchSrvStub as SearchSrv);
describe('Given an empty result', () => {
beforeEach(() => {
ctrl.results = [];
});
describe('When navigating down one step', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(1);
});
it('should not navigate', () => {
expect(ctrl.selectedIndex).toBe(0);
});
});
describe('When navigating up one step', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(-1);
});
it('should not navigate', () => {
expect(ctrl.selectedIndex).toBe(0);
});
});
});
describe('Given a result of one selected collapsed folder with no dashboards and a root folder with 2 dashboards', () => {
beforeEach(() => {
ctrl.results = [
{
id: 1,
title: 'folder',
items: [],
selected: true,
expanded: false,
toggle: (i: any) => (i.expanded = !i.expanded),
},
{
id: 0,
title: 'General',
items: [
{ id: 3, selected: false },
{ id: 5, selected: false },
],
selected: false,
expanded: true,
toggle: (i: any) => (i.expanded = !i.expanded),
},
];
});
describe('When navigating down one step', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(1);
});
it('should select first dashboard in root folder', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[1].selected).toBeFalsy();
expect(ctrl.results[1].items[0].selected).toBeTruthy();
expect(ctrl.results[1].items[1].selected).toBeFalsy();
});
});
describe('When navigating down two steps', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(1);
ctrl.moveSelection(1);
});
it('should select last dashboard in root folder', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[1].selected).toBeFalsy();
expect(ctrl.results[1].items[0].selected).toBeFalsy();
expect(ctrl.results[1].items[1].selected).toBeTruthy();
});
});
describe('When navigating down three steps', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(1);
ctrl.moveSelection(1);
ctrl.moveSelection(1);
});
it('should select first folder', () => {
expect(ctrl.results[0].selected).toBeTruthy();
expect(ctrl.results[1].selected).toBeFalsy();
expect(ctrl.results[1].items[0].selected).toBeFalsy();
expect(ctrl.results[1].items[1].selected).toBeFalsy();
});
});
describe('When navigating up one step', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(-1);
});
it('should select last dashboard in root folder', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[1].selected).toBeFalsy();
expect(ctrl.results[1].items[0].selected).toBeFalsy();
expect(ctrl.results[1].items[1].selected).toBeTruthy();
});
});
describe('When navigating up two steps', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(-1);
ctrl.moveSelection(-1);
});
it('should select first dashboard in root folder', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[1].selected).toBeFalsy();
expect(ctrl.results[1].items[0].selected).toBeTruthy();
expect(ctrl.results[1].items[1].selected).toBeFalsy();
});
});
});
describe('Given a result of one selected collapsed folder with 2 dashboards and a root folder with 2 dashboards', () => {
beforeEach(() => {
ctrl.results = [
{
id: 1,
title: 'folder',
items: [
{ id: 2, selected: false },
{ id: 4, selected: false },
],
selected: true,
expanded: false,
toggle: (i: any) => (i.expanded = !i.expanded),
},
{
id: 0,
title: 'General',
items: [
{ id: 3, selected: false },
{ id: 5, selected: false },
],
selected: false,
expanded: true,
toggle: (i: any) => (i.expanded = !i.expanded),
},
];
});
describe('When navigating down one step', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(1);
});
it('should select first dashboard in root folder', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[1].selected).toBeFalsy();
expect(ctrl.results[0].items[0].selected).toBeFalsy();
expect(ctrl.results[0].items[1].selected).toBeFalsy();
expect(ctrl.results[1].items[0].selected).toBeTruthy();
expect(ctrl.results[1].items[1].selected).toBeFalsy();
});
});
describe('When navigating down two steps', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(1);
ctrl.moveSelection(1);
});
it('should select last dashboard in root folder', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[1].selected).toBeFalsy();
expect(ctrl.results[0].items[0].selected).toBeFalsy();
expect(ctrl.results[0].items[1].selected).toBeFalsy();
expect(ctrl.results[1].items[0].selected).toBeFalsy();
expect(ctrl.results[1].items[1].selected).toBeTruthy();
});
});
describe('When navigating down three steps', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(1);
ctrl.moveSelection(1);
ctrl.moveSelection(1);
});
it('should select first folder', () => {
expect(ctrl.results[0].selected).toBeTruthy();
expect(ctrl.results[1].selected).toBeFalsy();
expect(ctrl.results[0].items[0].selected).toBeFalsy();
expect(ctrl.results[0].items[1].selected).toBeFalsy();
expect(ctrl.results[1].items[0].selected).toBeFalsy();
expect(ctrl.results[1].items[1].selected).toBeFalsy();
});
});
describe('When navigating up one step', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(-1);
});
it('should select last dashboard in root folder', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[1].selected).toBeFalsy();
expect(ctrl.results[0].items[0].selected).toBeFalsy();
expect(ctrl.results[0].items[1].selected).toBeFalsy();
expect(ctrl.results[1].items[0].selected).toBeFalsy();
expect(ctrl.results[1].items[1].selected).toBeTruthy();
});
});
describe('When navigating up two steps', () => {
beforeEach(() => {
ctrl.selectedIndex = 0;
ctrl.moveSelection(-1);
ctrl.moveSelection(-1);
});
it('should select first dashboard in root folder', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[1].selected).toBeFalsy();
expect(ctrl.results[1].items[0].selected).toBeTruthy();
expect(ctrl.results[1].items[1].selected).toBeFalsy();
});
});
});
describe('Given a result of a search with 2 dashboards where the first is selected', () => {
beforeEach(() => {
ctrl.results = [
{
hideHeader: true,
items: [
{ id: 3, selected: true },
{ id: 5, selected: false },
],
selected: false,
expanded: true,
toggle: (i: any) => (i.expanded = !i.expanded),
},
];
});
describe('When navigating down one step', () => {
beforeEach(() => {
ctrl.selectedIndex = 1;
ctrl.moveSelection(1);
});
it('should select last dashboard', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[0].items[0].selected).toBeFalsy();
expect(ctrl.results[0].items[1].selected).toBeTruthy();
});
});
describe('When navigating down two steps', () => {
beforeEach(() => {
ctrl.selectedIndex = 1;
ctrl.moveSelection(1);
ctrl.moveSelection(1);
});
it('should select first dashboard', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[0].items[0].selected).toBeTruthy();
expect(ctrl.results[0].items[1].selected).toBeFalsy();
});
});
describe('When navigating down three steps', () => {
beforeEach(() => {
ctrl.selectedIndex = 1;
ctrl.moveSelection(1);
ctrl.moveSelection(1);
ctrl.moveSelection(1);
});
it('should select last dashboard', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[0].items[0].selected).toBeFalsy();
expect(ctrl.results[0].items[1].selected).toBeTruthy();
});
});
describe('When navigating up one step', () => {
beforeEach(() => {
ctrl.selectedIndex = 1;
ctrl.moveSelection(-1);
});
it('should select last dashboard', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[0].items[0].selected).toBeFalsy();
expect(ctrl.results[0].items[1].selected).toBeTruthy();
});
});
describe('When navigating up two steps', () => {
beforeEach(() => {
ctrl.selectedIndex = 1;
ctrl.moveSelection(-1);
ctrl.moveSelection(-1);
});
it('should select first dashboard', () => {
expect(ctrl.results[0].selected).toBeFalsy();
expect(ctrl.results[0].items[0].selected).toBeTruthy();
expect(ctrl.results[0].items[1].selected).toBeFalsy();
});
});
});
});

View File

@ -1,149 +0,0 @@
import { ILocationService, IScope } from 'angular';
import { SearchResultsCtrl } from '../components/search/search_results';
import { afterEach, beforeEach } from 'test/lib/common';
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
jest.mock('app/core/app_events', () => {
return {
emit: jest.fn(),
};
});
describe('SearchResultsCtrl', () => {
let ctrl: any;
const $location = {} as ILocationService;
const $scope = ({ $evalAsync: jest.fn() } as any) as IScope;
describe('when checking an item that is not checked', () => {
const item = { checked: false };
let selectionChanged = false;
beforeEach(() => {
ctrl = new SearchResultsCtrl($location, $scope);
ctrl.onSelectionChanged = () => (selectionChanged = true);
ctrl.toggleSelection(item);
});
it('should set checked to true', () => {
expect(item.checked).toBeTruthy();
});
it('should trigger selection changed callback', () => {
expect(selectionChanged).toBeTruthy();
});
});
describe('when checking an item that is checked', () => {
const item = { checked: true };
let selectionChanged = false;
beforeEach(() => {
ctrl = new SearchResultsCtrl($location, $scope);
ctrl.onSelectionChanged = () => (selectionChanged = true);
ctrl.toggleSelection(item);
});
it('should set checked to false', () => {
expect(item.checked).toBeFalsy();
});
it('should trigger selection changed callback', () => {
expect(selectionChanged).toBeTruthy();
});
});
describe('when selecting a tag', () => {
let selectedTag: any = null;
beforeEach(() => {
ctrl = new SearchResultsCtrl($location, $scope);
ctrl.onTagSelected = (tag: any) => (selectedTag = tag);
ctrl.selectTag('tag-test');
});
it('should trigger tag selected callback', () => {
expect(selectedTag['$tag']).toBe('tag-test');
});
});
describe('when toggle a collapsed folder', () => {
let folderExpanded = false;
beforeEach(() => {
ctrl = new SearchResultsCtrl($location, $scope);
ctrl.onFolderExpanding = () => {
folderExpanded = true;
};
const folder = {
expanded: false,
toggle: () => Promise.resolve(folder),
};
ctrl.toggleFolderExpand(folder);
});
it('should trigger folder expanding callback', () => {
expect(folderExpanded).toBeTruthy();
});
});
describe('when toggle an expanded folder', () => {
let folderExpanded = false;
beforeEach(() => {
ctrl = new SearchResultsCtrl($location, $scope);
ctrl.onFolderExpanding = () => {
folderExpanded = true;
};
const folder = {
expanded: true,
toggle: () => Promise.resolve(folder),
};
ctrl.toggleFolderExpand(folder);
});
it('should not trigger folder expanding callback', () => {
expect(folderExpanded).toBeFalsy();
});
});
describe('when clicking on a link in search result', () => {
const dashPath = 'dashboard/path';
const $location = ({ path: () => dashPath } as any) as ILocationService;
const appEventsMock = appEvents as any;
describe('with the same url as current path', () => {
beforeEach(() => {
ctrl = new SearchResultsCtrl($location, $scope);
const item = { url: dashPath };
ctrl.onItemClick(item);
});
it('should close the search', () => {
expect(appEventsMock.emit.mock.calls.length).toBe(1);
expect(appEventsMock.emit.mock.calls[0][0]).toBe(CoreEvents.hideDashSearch);
});
});
describe('with a different url than current path', () => {
beforeEach(() => {
ctrl = new SearchResultsCtrl($location, $scope);
const item = { url: 'another/path' };
ctrl.onItemClick(item);
});
it('should do nothing', () => {
expect(appEventsMock.emit.mock.calls.length).toBe(0);
});
});
afterEach(() => {
appEventsMock.emit.mockClear();
});
});
});

View File

@ -1,10 +0,0 @@
import { NavModelSrv } from 'app/core/core';
export class DashboardListCtrl {
navModel: any;
/** @ngInject */
constructor(navModelSrv: NavModelSrv) {
this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0);
}
}

View File

@ -1,66 +0,0 @@
import { IScope } from 'angular';
import { AppEvents } from '@grafana/data';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { backendSrv } from 'app/core/services/backend_srv';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
export class MoveToFolderCtrl {
dashboards: any;
folder: any;
dismiss: any;
afterSave: any;
isValidFolderSelection = true;
constructor(private $scope: IScope) {}
onFolderChange = (folder: any) => {
this.folder = folder;
};
save = () => {
return promiseToDigest(this.$scope)(
backendSrv.moveDashboards(this.dashboards, this.folder).then((result: any) => {
if (result.successCount > 0) {
const header = `Dashboard${result.successCount === 1 ? '' : 's'} Moved`;
const msg = `${result.successCount} dashboard${result.successCount === 1 ? '' : 's'} moved to ${
this.folder.title
}`;
appEvents.emit(AppEvents.alertSuccess, [header, msg]);
}
if (result.totalCount === result.alreadyInFolderCount) {
appEvents.emit(AppEvents.alertError, ['Error', `Dashboards already belongs to folder ${this.folder.title}`]);
}
this.dismiss();
return this.afterSave();
})
);
};
onEnterFolderCreation = () => {
this.isValidFolderSelection = false;
};
onExitFolderCreation = () => {
this.isValidFolderSelection = true;
};
}
export function moveToFolderModal() {
return {
restrict: 'E',
templateUrl: 'public/app/features/manage-dashboards/components/MoveToFolderModal/template.html',
controller: MoveToFolderCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
dismiss: '&',
dashboards: '=',
afterSave: '&',
},
};
}
coreModule.directive('moveToFolderModal', moveToFolderModal);

View File

@ -1 +0,0 @@
export { MoveToFolderCtrl } from './MoveToFolderCtrl';

View File

@ -1,38 +0,0 @@
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<icon name="'folder-plus'" size="'lg'"></icon>
<span class="p-l-1">Choose Dashboard Folder</span>
</h2>
<a class="modal-header-close" ng-click="ctrl.dismiss();">
<icon name="'times'"></icon>
</a>
</div>
<form name="ctrl.saveForm" ng-submit="ctrl.save()" class="modal-content folder-modal" novalidate>
<p>Move the {{ctrl.dashboards.length}} selected dashboards to the following folder:</p>
<div class="p-t-2">
<div class="gf-form">
<folder-picker
on-change="ctrl.onFolderChange"
label-class="width-7"
enable-create-new="true"
folder="ctrl.folder"
>
</folder-picker>
</div>
</div>
<div class="gf-form-button-row text-center">
<button
type="submit"
class="btn btn-primary"
ng-disabled="ctrl.saveForm.$invalid || !ctrl.isValidFolderSelection"
>
Move
</button>
<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
</div>
</form>
</div>

View File

@ -2,14 +2,11 @@
export { ValidationSrv } from './services/ValidationSrv';
// Components
export * from './components/MoveToFolderModal';
export * from './components/UploadDashboard';
// Controllers
import { DashboardListCtrl } from './DashboardListCtrl';
import { SnapshotListCtrl } from './SnapshotListCtrl';
import coreModule from 'app/core/core_module';
coreModule.controller('DashboardListCtrl', DashboardListCtrl);
coreModule.controller('SnapshotListCtrl', SnapshotListCtrl);

View File

@ -23,15 +23,10 @@ export const MoveToFolderModal: FC<Props> = ({ results, onMoveItems, isOpen, onD
const selectedDashboards = getCheckedDashboards(results);
const moveTo = () => {
if (folder) {
if (folder && selectedDashboards.length) {
const folderTitle = folder.title ?? 'General';
backendSrv
.moveDashboards(
selectedDashboards.map(d => d.uid),
folder
)
.then((result: any) => {
backendSrv.moveDashboards(selectedDashboards.map(d => d.uid) as string[], folder).then((result: any) => {
if (result.successCount > 0) {
const ending = result.successCount === 1 ? '' : 's';
const header = `Dashboard${ending} Moved`;

View File

@ -1,4 +1,4 @@
import React, { FC, MutableRefObject } from 'react';
import React, { FC } from 'react';
import { css, cx } from 'emotion';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
@ -18,7 +18,6 @@ export interface Props {
onToggleSection: (section: DashboardSection) => void;
results: DashboardSection[];
layout?: string;
wrapperRef?: MutableRefObject<HTMLDivElement | null>;
}
export const SearchResults: FC<Props> = ({
@ -28,7 +27,6 @@ export const SearchResults: FC<Props> = ({
onToggleChecked,
onToggleSection,
results,
wrapperRef,
layout,
}) => {
const theme = useTheme();

View File

@ -36,7 +36,7 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
[section]
);
return !section.hideHeader ? (
return (
<div className={styles.wrapper} onClick={onSectionExpand}>
<SearchCheckbox editable={editable} checked={section.checked} onClick={onSectionChecked} />
<Icon className={styles.icon} name={section.icon as IconName} />
@ -49,8 +49,6 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
)}
<Icon name={section.expanded ? 'angle-down' : 'angle-right'} />
</div>
) : (
<div className={styles.wrapper} />
);
};

View File

@ -24,10 +24,6 @@ export const useSearch: UseSearch = (query, reducer, params) => {
dispatch({ type: SEARCH_START });
const parsedQuery = getParsedQuery(query, queryParsing);
searchSrv.search(parsedQuery).then(results => {
// Remove header for folder search
if (query.folderIds.length === 1 && results.length) {
results[0].hideHeader = true;
}
dispatch({ type: FETCH_RESULTS, payload: results });
if (searchCallback) {

View File

@ -7,4 +7,3 @@ export { SearchResultsFilter } from './components/SearchResultsFilter';
export { ManageDashboards } from './components/ManageDashboards';
export { ConfirmDeleteModal } from './components/ConfirmDeleteModal';
export { MoveToFolderModal } from './components/MoveToFolderModal';
export * from './types';

View File

@ -15,14 +15,14 @@ export interface DashboardSection {
title: string;
expanded?: boolean;
url: string;
icon: string;
score: number;
hideHeader?: boolean;
icon?: string;
score?: number;
checked?: boolean;
items: DashboardSectionItem[];
toggle?: (section: DashboardSection) => Promise<DashboardSection>;
selected?: boolean;
type: DashboardSearchItemType;
slug?: string;
}
export interface DashboardSectionItem {
@ -37,11 +37,13 @@ export interface DashboardSectionItem {
tags: string[];
title: string;
type: DashboardSearchItemType;
uid: string;
uid?: string;
uri: string;
url: string;
}
export interface DashboardSearchHit extends DashboardSectionItem, DashboardSection {}
export interface DashboardTag {
term: string;
count: number;

View File

@ -10,7 +10,6 @@ export * from './datasources';
export * from './plugins';
export * from './organization';
export * from './appNotifications';
export * from './search';
export * from './explore';
export * from './store';
export * from './ldap';

View File

@ -1,20 +0,0 @@
export enum DashboardSearchHitType {
DashHitDB = 'dash-db',
DashHitHome = 'dash-home',
DashHitFolder = 'dash-folder',
}
export interface DashboardSearchHit {
folderId?: number;
folderTitle?: string;
folderUid?: string;
folderUrl?: string;
id: number;
isStarred: boolean;
slug: string;
tags: string[];
title: string;
type: DashboardSearchHitType;
uid: string;
uri: string;
url: string;
}