diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts
index fef6935f685..4d0fc23c142 100644
--- a/public/app/core/angular_wrappers.ts
+++ b/public/app/core/angular_wrappers.ts
@@ -28,7 +28,7 @@ import {
SaveDashboardButtonConnected,
} from '../features/dashboard/components/SaveDashboard/SaveDashboardButton';
import { VariableEditorContainer } from '../features/variables/editor/VariableEditorContainer';
-import { SearchField, SearchResults } from '../features/search';
+import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
export function registerAngularDirectives() {
react2AngularDirective('footer', Footer, []);
@@ -66,6 +66,19 @@ export function registerAngularDirectives() {
['onFolderExpanding', { watchDepth: 'reference' }],
['onToggleSelection', { watchDepth: 'reference' }],
]);
+ react2AngularDirective('searchFilters', SearchResultsFilter, [
+ 'allChecked',
+ 'canMove',
+ 'canDelete',
+ 'tagFilterOptions',
+ 'selectedStarredFilter',
+ 'selectedTagFilter',
+ ['onSelectAllChanged', { watchDepth: 'reference' }],
+ ['deleteItem', { watchDepth: 'reference' }],
+ ['moveTo', { watchDepth: 'reference' }],
+ ['onStarredFilterChange', { watchDepth: 'reference' }],
+ ['onTagFilterChange', { watchDepth: 'reference' }],
+ ]);
react2AngularDirective('tagFilter', TagFilter, [
'tags',
['onChange', { watchDepth: 'reference' }],
diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html
index 9c21324e3ae..dd4f11246fa 100644
--- a/public/app/core/components/manage_dashboards/manage_dashboards.html
+++ b/public/app/core/components/manage_dashboards/manage_dashboards.html
@@ -60,47 +60,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
{
this.initTagFilter();
});
@@ -185,7 +183,7 @@ export class ManageDashboardsCtrl {
return ids;
}
- delete() {
+ delete = () => {
const data = this.getFoldersAndDashboardsToDelete();
const folderCount = data.folderUids.length;
const dashCount = data.dashboardUids.length;
@@ -211,7 +209,7 @@ export class ManageDashboardsCtrl {
this.deleteFoldersAndDashboards(data.folderUids, data.dashboardUids);
},
});
- }
+ };
private deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) {
promiseToDigest(this.$scope)(
@@ -232,7 +230,7 @@ export class ManageDashboardsCtrl {
return selectedDashboards;
}
- moveTo() {
+ moveTo = () => {
const selectedDashboards = this.getDashboardsToMove();
const template =
@@ -247,12 +245,11 @@ export class ManageDashboardsCtrl {
afterSave: this.refreshList.bind(this),
},
});
- }
+ };
initTagFilter() {
return this.searchSrv.getDashboardTags().then((results: any) => {
- this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results);
- this.selectedTagFilter = this.tagFilterOptions[0];
+ this.tagFilterOptions = results.map((result: any) => ({ value: result.term, label: result.term }));
});
}
@@ -269,11 +266,11 @@ export class ManageDashboardsCtrl {
return this.refreshList();
}
- onTagFilterChange() {
- const res = this.filterByTag(this.selectedTagFilter.term);
- this.selectedTagFilter = this.tagFilterOptions[0];
+ 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);
@@ -289,13 +286,15 @@ export class ManageDashboardsCtrl {
return this.refreshList();
}
- onStarredFilterChange() {
- this.query.starred = this.selectedStarredFilter.text === 'Yes';
- this.selectedStarredFilter = this.starredFilterOptions[0];
+ onStarredFilterChange = (filter: SelectableValue) => {
+ this.query.starred = filter.value;
+ this.selectedStarredFilter = filter.value;
return this.refreshList();
- }
+ };
+
+ onSelectAllChanged = () => {
+ this.selectAllChecked = !this.selectAllChecked;
- onSelectAllChanged() {
for (const section of this.sections) {
if (!section.hideHeader) {
section.checked = this.selectAllChecked;
@@ -306,14 +305,15 @@ export class ManageDashboardsCtrl {
return item;
});
}
-
this.selectionChanged();
- }
+ };
clearFilters() {
this.query.query = '';
this.query.tag = [];
this.query.starred = false;
+ this.selectedStarredFilter = 'starred';
+ this.selectedTagFilter = 'tag';
this.refreshList();
}
diff --git a/public/app/core/specs/manage_dashboards.test.ts b/public/app/core/specs/manage_dashboards.test.ts
index 9c179cc14f3..05bcd4673c7 100644
--- a/public/app/core/specs/manage_dashboards.test.ts
+++ b/public/app/core/specs/manage_dashboards.test.ts
@@ -188,7 +188,7 @@ describe('ManageDashboards', () => {
describe('when select all is checked', () => {
beforeEach(() => {
- ctrl.selectAllChecked = true;
+ ctrl.selectAllChecked = false;
ctrl.onSelectAllChanged();
});
@@ -245,10 +245,10 @@ describe('ManageDashboards', () => {
describe('with starred filter', () => {
beforeEach(() => {
- const yesOption: any = ctrl.starredFilterOptions[1];
+ const yesOption: any = { label: 'Yes', value: true };
ctrl.selectedStarredFilter = yesOption;
- return ctrl.onStarredFilterChange();
+ return ctrl.onStarredFilterChange(yesOption);
});
it('should set starred filter', () => {
@@ -306,7 +306,7 @@ describe('ManageDashboards', () => {
describe('when select all is checked', () => {
beforeEach(() => {
- ctrl.selectAllChecked = true;
+ ctrl.selectAllChecked = false;
ctrl.onSelectAllChanged();
});
@@ -354,7 +354,7 @@ describe('ManageDashboards', () => {
describe('when select all is unchecked', () => {
beforeEach(() => {
- ctrl.selectAllChecked = false;
+ ctrl.selectAllChecked = true;
ctrl.onSelectAllChanged();
});
diff --git a/public/app/features/search/components/SearchResultsFilter.test.tsx b/public/app/features/search/components/SearchResultsFilter.test.tsx
new file mode 100644
index 00000000000..64ea1f73066
--- /dev/null
+++ b/public/app/features/search/components/SearchResultsFilter.test.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import { mount, shallow } from 'enzyme';
+import { SearchResultsFilter, Props } from './SearchResultsFilter';
+
+const noop = jest.fn();
+
+const findBtnByText = (wrapper: any, text: string) =>
+ wrapper.findWhere((c: any) => c.name() === 'Button' && c.text() === text);
+
+const setup = (propOverrides?: Partial, renderMethod = shallow) => {
+ const props: Props = {
+ //@ts-ignore
+ allChecked: false,
+ canDelete: false,
+ canMove: false,
+ deleteItem: noop,
+ moveTo: noop,
+ onSelectAllChanged: noop,
+ onStarredFilterChange: noop,
+ onTagFilterChange: noop,
+ selectedStarredFilter: 'starred',
+ selectedTagFilter: 'tag',
+ tagFilterOptions: [],
+ };
+
+ Object.assign(props, propOverrides);
+
+ const wrapper = renderMethod();
+ const instance = wrapper.instance();
+
+ return {
+ wrapper,
+ instance,
+ };
+};
+
+describe('SearchResultsFilter', () => {
+ it('should render "filter by starred" and "filter by tag" filters by default', () => {
+ const { wrapper } = setup();
+ expect(wrapper.find({ placeholder: 'Filter by starred' })).toHaveLength(1);
+ expect(wrapper.find({ placeholder: 'Filter by tag' })).toHaveLength(1);
+ expect(findBtnByText(wrapper, 'Move')).toHaveLength(0);
+ expect(findBtnByText(wrapper, 'Delete')).toHaveLength(0);
+ });
+
+ it('should render Move and Delete buttons when canDelete is true', () => {
+ const { wrapper } = setup({ canDelete: true });
+ expect(wrapper.find({ placeholder: 'Filter by starred' })).toHaveLength(0);
+ expect(wrapper.find({ placeholder: 'Filter by tag' })).toHaveLength(0);
+ expect(findBtnByText(wrapper, 'Move')).toHaveLength(1);
+ expect(findBtnByText(wrapper, 'Delete')).toHaveLength(1);
+ });
+
+ it('should render Move and Delete buttons when canMove is true', () => {
+ const { wrapper } = setup({ canMove: true });
+ expect(wrapper.find({ placeholder: 'Filter by starred' })).toHaveLength(0);
+ expect(wrapper.find({ placeholder: 'Filter by tag' })).toHaveLength(0);
+ expect(findBtnByText(wrapper, 'Move')).toHaveLength(1);
+ expect(findBtnByText(wrapper, 'Delete')).toHaveLength(1);
+ });
+
+ it('should be called with proper filter option when "filter by starred" is changed', () => {
+ const mockFilterStarred = jest.fn();
+ const option = { value: true, label: 'Yes' };
+ //@ts-ignore
+ const { wrapper } = setup({ onStarredFilterChange: mockFilterStarred }, mount);
+ wrapper
+ .find({ placeholder: 'Filter by starred' })
+ .at(0)
+ .prop('onChange')(option);
+ expect(mockFilterStarred).toHaveBeenCalledTimes(1);
+ expect(mockFilterStarred).toHaveBeenCalledWith(option);
+ });
+
+ it('should be called with proper filter option when "filter by tags" is changed', () => {
+ const mockFilterByTags = jest.fn();
+ const tags = [
+ { value: 'tag1', label: 'Tag 1' },
+ { value: 'tag2', label: 'Tag 2' },
+ ];
+ //@ts-ignore
+ const { wrapper } = setup({ onTagFilterChange: mockFilterByTags, tagFilterOptions: tags }, mount);
+ wrapper
+ .find({ placeholder: 'Filter by tag' })
+ .at(0)
+ .prop('onChange')(tags[0]);
+ expect(mockFilterByTags).toHaveBeenCalledTimes(1);
+ expect(mockFilterByTags).toHaveBeenCalledWith(tags[0]);
+ });
+
+ it('should call "onSelectAllChanged" when checkbox is changed', () => {
+ const mockSelectAll = jest.fn();
+ const { wrapper } = setup({ onSelectAllChanged: mockSelectAll });
+ wrapper.find('Checkbox').simulate('change');
+ expect(mockSelectAll).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/public/app/features/search/components/SearchResultsFilter.tsx b/public/app/features/search/components/SearchResultsFilter.tsx
new file mode 100644
index 00000000000..cd6331aed93
--- /dev/null
+++ b/public/app/features/search/components/SearchResultsFilter.tsx
@@ -0,0 +1,93 @@
+import React, { FC } from 'react';
+import { css } from 'emotion';
+import { Button, Forms, stylesFactory, useTheme, HorizontalGroup } from '@grafana/ui';
+import { GrafanaTheme, SelectableValue } from '@grafana/data';
+
+type onSelectChange = (value: SelectableValue) => void;
+
+export interface Props {
+ allChecked?: boolean;
+ canDelete?: boolean;
+ canMove?: boolean;
+ deleteItem: () => void;
+ moveTo: () => void;
+ onSelectAllChanged: any;
+ onStarredFilterChange: onSelectChange;
+ onTagFilterChange: onSelectChange;
+ selectedStarredFilter: string;
+ selectedTagFilter: string;
+ tagFilterOptions: SelectableValue[];
+}
+
+const starredFilterOptions = [
+ { label: 'Yes', value: true },
+ { label: 'No', value: false },
+];
+
+export const SearchResultsFilter: FC = ({
+ allChecked,
+ canDelete,
+ canMove,
+ deleteItem,
+ moveTo,
+ onSelectAllChanged,
+ onStarredFilterChange,
+ onTagFilterChange,
+ selectedStarredFilter,
+ selectedTagFilter,
+ tagFilterOptions,
+}) => {
+ const showActions = canDelete || canMove;
+ const theme = useTheme();
+ const styles = getStyles(theme);
+
+ return (
+
+
+ {showActions ? (
+
+
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+ );
+};
+
+const getStyles = stylesFactory((theme: GrafanaTheme) => {
+ return {
+ wrapper: css`
+ height: 35px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ label {
+ height: 20px;
+ margin-left: 8px;
+ }
+ `,
+ };
+});
diff --git a/public/app/features/search/index.ts b/public/app/features/search/index.ts
index 578d36df252..9b296c6709a 100644
--- a/public/app/features/search/index.ts
+++ b/public/app/features/search/index.ts
@@ -2,4 +2,5 @@ export { SearchResults } from './components/SearchResults';
export { SearchField } from './components/SearchField';
export { SearchItem } from './components/SearchItem';
export { SearchCheckbox } from './components/SearchCheckbox';
+export { SearchResultsFilter } from './components/SearchResultsFilter';
export * from './types';