From ce3a1fc56c61516eeaf9f5a074ed206899d530e6 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 2 Apr 2020 14:07:31 +0300 Subject: [PATCH] Search/migrate search filter actions (#23133) * Search: Initial setup * Search: Use icon prop * Search: Add button variants * Search: Enable toggle all * Search: Fix starred filter * Search: update tests * Search: Enable filters * Search: Use emotion styling * Search: Enable dashboard deleting * Search: Enable dashboard moving * Search: Update tests * Search: Add SearchResultsFilter.test.tsx * Search: Tweak types * Search: Remove onReset * Search: Remove redundant fragment * Search: Use HorizontalGroup * Search: Alight top checkbox --- public/app/core/angular_wrappers.ts | 15 ++- .../manage_dashboards/manage_dashboards.html | 54 +++-------- .../manage_dashboards/manage_dashboards.ts | 40 ++++---- .../app/core/specs/manage_dashboards.test.ts | 10 +- .../components/SearchResultsFilter.test.tsx | 97 +++++++++++++++++++ .../search/components/SearchResultsFilter.tsx | 93 ++++++++++++++++++ public/app/features/search/index.ts | 1 + 7 files changed, 243 insertions(+), 67 deletions(-) create mode 100644 public/app/features/search/components/SearchResultsFilter.test.tsx create mode 100644 public/app/features/search/components/SearchResultsFilter.tsx 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';