Commit ce3a1fc5 by Alex Khomenko Committed by GitHub

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
parent 9c9f6f16
...@@ -28,7 +28,7 @@ import { ...@@ -28,7 +28,7 @@ import {
SaveDashboardButtonConnected, SaveDashboardButtonConnected,
} from '../features/dashboard/components/SaveDashboard/SaveDashboardButton'; } from '../features/dashboard/components/SaveDashboard/SaveDashboardButton';
import { VariableEditorContainer } from '../features/variables/editor/VariableEditorContainer'; import { VariableEditorContainer } from '../features/variables/editor/VariableEditorContainer';
import { SearchField, SearchResults } from '../features/search'; import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
export function registerAngularDirectives() { export function registerAngularDirectives() {
react2AngularDirective('footer', Footer, []); react2AngularDirective('footer', Footer, []);
...@@ -66,6 +66,19 @@ export function registerAngularDirectives() { ...@@ -66,6 +66,19 @@ export function registerAngularDirectives() {
['onFolderExpanding', { watchDepth: 'reference' }], ['onFolderExpanding', { watchDepth: 'reference' }],
['onToggleSelection', { 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, [ react2AngularDirective('tagFilter', TagFilter, [
'tags', 'tags',
['onChange', { watchDepth: 'reference' }], ['onChange', { watchDepth: 'reference' }],
......
...@@ -60,47 +60,19 @@ ...@@ -60,47 +60,19 @@
</div> </div>
<div class="search-results" ng-show="ctrl.sections.length > 0"> <div class="search-results" ng-show="ctrl.sections.length > 0">
<div class="search-results-filter-row"> <search-filters
<gf-form-checkbox on-select-all-changed="ctrl.onSelectAllChanged"
on-change="ctrl.onSelectAllChanged()" all-checked="ctrl.selectAllChecked"
checked="ctrl.selectAllChecked" can-move="ctrl.canMove"
switch-class="gf-form-checkbox--transparent" 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-filter-row__filters">
<div class="gf-form-select-wrapper" ng-show="!(ctrl.canMove || ctrl.canDelete)">
<select
class="search-results-filter-row__filters-item gf-form-input"
ng-model="ctrl.selectedStarredFilter"
ng-options="t.text disable when t.disabled for t in ctrl.starredFilterOptions"
ng-change="ctrl.onStarredFilterChange()"
/>
</div>
<div class="gf-form-select-wrapper" ng-show="!(ctrl.canMove || ctrl.canDelete)">
<select
class="search-results-filter-row__filters-item gf-form-input"
ng-model="ctrl.selectedTagFilter"
ng-options="t.term disable when t.disabled for t in ctrl.tagFilterOptions"
ng-change="ctrl.onTagFilterChange()"
/>
</div>
<div class="gf-form-button-row" ng-show="ctrl.canMove || ctrl.canDelete">
<button type="button"
class="btn gf-form-button btn-inverse"
ng-disabled="!ctrl.canMove"
ng-click="ctrl.moveTo()"
bs-tooltip="ctrl.canMove ? '' : 'Select a dashboard to move (cannot move folders)'"
data-placement="bottom">
<i class="fa fa-exchange"></i>&nbsp;&nbsp;Move
</button>
<button type="button"
class="btn gf-form-button btn-danger"
ng-click="ctrl.delete()"
ng-disabled="!ctrl.canDelete">
<i class="fa fa-trash"></i>&nbsp;&nbsp;Delete
</button>
</div>
</div>
</div>
<div class="search-results-container"> <div class="search-results-container">
<search-results <search-results
results="ctrl.sections" results="ctrl.sections"
......
import { IScope } from 'angular'; import { IScope } from 'angular';
import _ from 'lodash'; import _ from 'lodash';
import { SelectableValue } from '@grafana/data';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { SearchSrv } from 'app/core/services/search_srv'; import { SearchSrv } from 'app/core/services/search_srv';
...@@ -54,7 +55,6 @@ export class ManageDashboardsCtrl { ...@@ -54,7 +55,6 @@ export class ManageDashboardsCtrl {
hasFilters = false; hasFilters = false;
tagFilterOptions: any[]; tagFilterOptions: any[];
selectedTagFilter: any; selectedTagFilter: any;
starredFilterOptions = [{ text: 'Filter by Starred', disabled: true }, { text: 'Yes' }, { text: 'No' }];
selectedStarredFilter: any; selectedStarredFilter: any;
// used when managing dashboards for a specific folder // used when managing dashboards for a specific folder
...@@ -88,8 +88,6 @@ export class ManageDashboardsCtrl { ...@@ -88,8 +88,6 @@ export class ManageDashboardsCtrl {
this.query.folderIds = [this.folderId]; this.query.folderIds = [this.folderId];
} }
this.selectedStarredFilter = this.starredFilterOptions[0];
this.refreshList().then(() => { this.refreshList().then(() => {
this.initTagFilter(); this.initTagFilter();
}); });
...@@ -185,7 +183,7 @@ export class ManageDashboardsCtrl { ...@@ -185,7 +183,7 @@ export class ManageDashboardsCtrl {
return ids; return ids;
} }
delete() { delete = () => {
const data = this.getFoldersAndDashboardsToDelete(); const data = this.getFoldersAndDashboardsToDelete();
const folderCount = data.folderUids.length; const folderCount = data.folderUids.length;
const dashCount = data.dashboardUids.length; const dashCount = data.dashboardUids.length;
...@@ -211,7 +209,7 @@ export class ManageDashboardsCtrl { ...@@ -211,7 +209,7 @@ export class ManageDashboardsCtrl {
this.deleteFoldersAndDashboards(data.folderUids, data.dashboardUids); this.deleteFoldersAndDashboards(data.folderUids, data.dashboardUids);
}, },
}); });
} };
private deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) { private deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) {
promiseToDigest(this.$scope)( promiseToDigest(this.$scope)(
...@@ -232,7 +230,7 @@ export class ManageDashboardsCtrl { ...@@ -232,7 +230,7 @@ export class ManageDashboardsCtrl {
return selectedDashboards; return selectedDashboards;
} }
moveTo() { moveTo = () => {
const selectedDashboards = this.getDashboardsToMove(); const selectedDashboards = this.getDashboardsToMove();
const template = const template =
...@@ -247,12 +245,11 @@ export class ManageDashboardsCtrl { ...@@ -247,12 +245,11 @@ export class ManageDashboardsCtrl {
afterSave: this.refreshList.bind(this), afterSave: this.refreshList.bind(this),
}, },
}); });
} };
initTagFilter() { initTagFilter() {
return this.searchSrv.getDashboardTags().then((results: any) => { return this.searchSrv.getDashboardTags().then((results: any) => {
this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results); this.tagFilterOptions = results.map((result: any) => ({ value: result.term, label: result.term }));
this.selectedTagFilter = this.tagFilterOptions[0];
}); });
} }
...@@ -269,11 +266,11 @@ export class ManageDashboardsCtrl { ...@@ -269,11 +266,11 @@ export class ManageDashboardsCtrl {
return this.refreshList(); return this.refreshList();
} }
onTagFilterChange() { onTagFilterChange = (filter: SelectableValue) => {
const res = this.filterByTag(this.selectedTagFilter.term); const res = this.filterByTag(filter.value);
this.selectedTagFilter = this.tagFilterOptions[0]; this.selectedTagFilter = filter.value;
return res; return res;
} };
removeTag(tag: any, evt: Event) { removeTag(tag: any, evt: Event) {
this.query.tag = _.without(this.query.tag, tag); this.query.tag = _.without(this.query.tag, tag);
...@@ -289,13 +286,15 @@ export class ManageDashboardsCtrl { ...@@ -289,13 +286,15 @@ export class ManageDashboardsCtrl {
return this.refreshList(); return this.refreshList();
} }
onStarredFilterChange() { onStarredFilterChange = (filter: SelectableValue) => {
this.query.starred = this.selectedStarredFilter.text === 'Yes'; this.query.starred = filter.value;
this.selectedStarredFilter = this.starredFilterOptions[0]; this.selectedStarredFilter = filter.value;
return this.refreshList(); return this.refreshList();
} };
onSelectAllChanged = () => {
this.selectAllChecked = !this.selectAllChecked;
onSelectAllChanged() {
for (const section of this.sections) { for (const section of this.sections) {
if (!section.hideHeader) { if (!section.hideHeader) {
section.checked = this.selectAllChecked; section.checked = this.selectAllChecked;
...@@ -306,14 +305,15 @@ export class ManageDashboardsCtrl { ...@@ -306,14 +305,15 @@ export class ManageDashboardsCtrl {
return item; return item;
}); });
} }
this.selectionChanged(); this.selectionChanged();
} };
clearFilters() { clearFilters() {
this.query.query = ''; this.query.query = '';
this.query.tag = []; this.query.tag = [];
this.query.starred = false; this.query.starred = false;
this.selectedStarredFilter = 'starred';
this.selectedTagFilter = 'tag';
this.refreshList(); this.refreshList();
} }
......
...@@ -188,7 +188,7 @@ describe('ManageDashboards', () => { ...@@ -188,7 +188,7 @@ describe('ManageDashboards', () => {
describe('when select all is checked', () => { describe('when select all is checked', () => {
beforeEach(() => { beforeEach(() => {
ctrl.selectAllChecked = true; ctrl.selectAllChecked = false;
ctrl.onSelectAllChanged(); ctrl.onSelectAllChanged();
}); });
...@@ -245,10 +245,10 @@ describe('ManageDashboards', () => { ...@@ -245,10 +245,10 @@ describe('ManageDashboards', () => {
describe('with starred filter', () => { describe('with starred filter', () => {
beforeEach(() => { beforeEach(() => {
const yesOption: any = ctrl.starredFilterOptions[1]; const yesOption: any = { label: 'Yes', value: true };
ctrl.selectedStarredFilter = yesOption; ctrl.selectedStarredFilter = yesOption;
return ctrl.onStarredFilterChange(); return ctrl.onStarredFilterChange(yesOption);
}); });
it('should set starred filter', () => { it('should set starred filter', () => {
...@@ -306,7 +306,7 @@ describe('ManageDashboards', () => { ...@@ -306,7 +306,7 @@ describe('ManageDashboards', () => {
describe('when select all is checked', () => { describe('when select all is checked', () => {
beforeEach(() => { beforeEach(() => {
ctrl.selectAllChecked = true; ctrl.selectAllChecked = false;
ctrl.onSelectAllChanged(); ctrl.onSelectAllChanged();
}); });
...@@ -354,7 +354,7 @@ describe('ManageDashboards', () => { ...@@ -354,7 +354,7 @@ describe('ManageDashboards', () => {
describe('when select all is unchecked', () => { describe('when select all is unchecked', () => {
beforeEach(() => { beforeEach(() => {
ctrl.selectAllChecked = false; ctrl.selectAllChecked = true;
ctrl.onSelectAllChanged(); ctrl.onSelectAllChanged();
}); });
......
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<Props>, 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(<SearchResultsFilter {...props} />);
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);
});
});
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<Props> = ({
allChecked,
canDelete,
canMove,
deleteItem,
moveTo,
onSelectAllChanged,
onStarredFilterChange,
onTagFilterChange,
selectedStarredFilter,
selectedTagFilter,
tagFilterOptions,
}) => {
const showActions = canDelete || canMove;
const theme = useTheme();
const styles = getStyles(theme);
return (
<div className={styles.wrapper}>
<Forms.Checkbox value={allChecked} onChange={onSelectAllChanged} />
{showActions ? (
<HorizontalGroup spacing="md">
<Button disabled={!canMove} onClick={moveTo} icon="fa fa-exchange" variant="secondary">
Move
</Button>
<Button disabled={!canDelete} onClick={deleteItem} icon="fa fa-trash" variant="destructive">
Delete
</Button>
</HorizontalGroup>
) : (
<HorizontalGroup spacing="md">
<Forms.Select
size="sm"
placeholder="Filter by starred"
key={selectedStarredFilter}
options={starredFilterOptions}
onChange={onStarredFilterChange}
/>
<Forms.Select
size="sm"
placeholder="Filter by tag"
key={selectedTagFilter}
options={tagFilterOptions}
onChange={onTagFilterChange}
/>
</HorizontalGroup>
)}
</div>
);
};
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;
}
`,
};
});
...@@ -2,4 +2,5 @@ export { SearchResults } from './components/SearchResults'; ...@@ -2,4 +2,5 @@ export { SearchResults } from './components/SearchResults';
export { SearchField } from './components/SearchField'; export { SearchField } from './components/SearchField';
export { SearchItem } from './components/SearchItem'; export { SearchItem } from './components/SearchItem';
export { SearchCheckbox } from './components/SearchCheckbox'; export { SearchCheckbox } from './components/SearchCheckbox';
export { SearchResultsFilter } from './components/SearchResultsFilter';
export * from './types'; export * from './types';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment