aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-07-27 12:18:59 +0200
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-08-14 11:44:44 +0200
commita0ea568244db7b77b165c7ecf7dca83620f8edbd (patch)
tree02c361c952478ae4ab28f9d0a2a6c6f9ff9ef4c5 /server/sonar-web/src/main/js/components
parentf2c95a685a3b950a68122af1e5673978a07df773 (diff)
downloadsonarqube-a0ea568244db7b77b165c7ecf7dca83620f8edbd.tar.gz
sonarqube-a0ea568244db7b77b165c7ecf7dca83620f8edbd.zip
Generalize facet components for both issues and measures
Diffstat (limited to 'server/sonar-web/src/main/js/components')
-rw-r--r--server/sonar-web/src/main/js/components/controls/SearchSelect.js124
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.js49
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap52
-rw-r--r--server/sonar-web/src/main/js/components/facet/FacetBox.js33
-rw-r--r--server/sonar-web/src/main/js/components/facet/FacetFooter.js43
-rw-r--r--server/sonar-web/src/main/js/components/facet/FacetHeader.js105
-rw-r--r--server/sonar-web/src/main/js/components/facet/FacetItem.js73
-rw-r--r--server/sonar-web/src/main/js/components/facet/FacetItemsList.js33
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.js33
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.js27
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.js65
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.js60
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.js33
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.js.snap9
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.js.snap15
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.js.snap192
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.js.snap93
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.js.snap9
18 files changed, 1048 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/components/controls/SearchSelect.js b/server/sonar-web/src/main/js/components/controls/SearchSelect.js
new file mode 100644
index 00000000000..38515273b6c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/SearchSelect.js
@@ -0,0 +1,124 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Select from 'react-select';
+import { debounce } from 'lodash';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+
+type Option = { label: string, value: string };
+
+type Props = {|
+ autofocus: boolean,
+ minimumQueryLength: number,
+ onSearch: (query: string) => Promise<Array<Option>>,
+ onSelect: (value: string) => void,
+ renderOption?: (option: Object) => React.Element<*>,
+ resetOnBlur: boolean,
+ value?: string
+|};
+
+type State = {
+ loading: boolean,
+ options: Array<Option>,
+ query: string
+};
+
+export default class SearchSelect extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ static defaultProps = {
+ autofocus: true,
+ minimumQueryLength: 2,
+ resetOnBlur: true
+ };
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { loading: false, options: [], query: '' };
+ this.search = debounce(this.search, 250);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ search = (query: string) => {
+ this.props.onSearch(query).then(options => {
+ if (this.mounted) {
+ this.setState({ loading: false, options });
+ }
+ });
+ };
+
+ handleBlur = () => {
+ this.setState({ options: [], query: '' });
+ };
+
+ handleChange = (option: Option) => {
+ this.props.onSelect(option.value);
+ };
+
+ handleInputChange = (query: string = '') => {
+ if (query.length >= this.props.minimumQueryLength) {
+ this.setState({ loading: true, query });
+ this.search(query);
+ } else {
+ this.setState({ options: [], query });
+ }
+ };
+
+ // disable internal filtering
+ handleFilterOption = () => true;
+
+ render() {
+ return (
+ <Select
+ autofocus={this.props.autofocus}
+ cache={false}
+ className="input-super-large"
+ clearable={false}
+ filterOption={this.handleFilterOption}
+ isLoading={this.state.loading}
+ noResultsText={
+ this.state.query.length < this.props.minimumQueryLength
+ ? translateWithParameters('select2.tooShort', this.props.minimumQueryLength)
+ : translate('select2.noMatches')
+ }
+ onBlur={this.props.resetOnBlur ? this.handleBlur : undefined}
+ onChange={this.handleChange}
+ onInputChange={this.handleInputChange}
+ onOpen={this.props.minimumQueryLength === 0 ? this.handleInputChange : undefined}
+ optionRenderer={this.props.renderOption}
+ options={this.state.options}
+ placeholder={translate('search_verb')}
+ searchable={true}
+ value={this.props.value}
+ valueRenderer={this.props.renderOption}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.js b/server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.js
new file mode 100644
index 00000000000..f4d46d4f8a8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.js
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import SearchSelect from '../SearchSelect';
+
+jest.mock('lodash', () => ({
+ debounce: fn => fn
+}));
+
+it('should render Select', () => {
+ expect(shallow(<SearchSelect onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
+});
+
+it('should call onSelect', () => {
+ const onSelect = jest.fn();
+ const wrapper = shallow(<SearchSelect onSearch={jest.fn()} onSelect={onSelect} />);
+ wrapper.prop('onChange')({ value: 'foo' });
+ expect(onSelect).lastCalledWith('foo');
+});
+
+it('should call onSearch', () => {
+ const onSearch = jest.fn().mockReturnValue(Promise.resolve([]));
+ const wrapper = shallow(
+ <SearchSelect minimumQueryLength={2} onSearch={onSearch} onSelect={jest.fn()} />
+ );
+ wrapper.prop('onInputChange')('f');
+ expect(onSearch).not.toHaveBeenCalled();
+ wrapper.prop('onInputChange')('foo');
+ expect(onSearch).lastCalledWith('foo');
+});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap
new file mode 100644
index 00000000000..d3ea2edb7b0
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render Select 1`] = `
+<Select
+ addLabelText="Add \\"{label}\\"?"
+ arrowRenderer={[Function]}
+ autofocus={true}
+ autosize={true}
+ backspaceRemoves={true}
+ backspaceToRemoveMessage="Press backspace to remove {label}"
+ cache={false}
+ className="input-super-large"
+ clearAllText="Clear all"
+ clearRenderer={[Function]}
+ clearValueText="Clear value"
+ clearable={false}
+ deleteRemoves={true}
+ delimiter=","
+ disabled={false}
+ escapeClearsValue={true}
+ filterOption={[Function]}
+ filterOptions={[Function]}
+ ignoreAccents={true}
+ ignoreCase={true}
+ inputProps={Object {}}
+ isLoading={false}
+ joinValues={false}
+ labelKey="label"
+ matchPos="any"
+ matchProp="any"
+ menuBuffer={0}
+ menuRenderer={[Function]}
+ multi={false}
+ noResultsText="select2.tooShort.2"
+ onBlur={[Function]}
+ onBlurResetsInput={true}
+ onChange={[Function]}
+ onCloseResetsInput={true}
+ onInputChange={[Function]}
+ optionComponent={[Function]}
+ options={Array []}
+ pageSize={5}
+ placeholder="search_verb"
+ required={false}
+ scrollMenuIntoView={true}
+ searchable={true}
+ simpleValue={false}
+ tabSelectsValue={true}
+ valueComponent={[Function]}
+ valueKey="value"
+/>
+`;
diff --git a/server/sonar-web/src/main/js/components/facet/FacetBox.js b/server/sonar-web/src/main/js/components/facet/FacetBox.js
new file mode 100644
index 00000000000..92b7afb58db
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/FacetBox.js
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+
+type Props = {|
+ children?: React.Element<*>
+|};
+
+export default function FacetBox(props: Props) {
+ return (
+ <div className="search-navigator-facet-box">
+ {props.children}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/facet/FacetFooter.js b/server/sonar-web/src/main/js/components/facet/FacetFooter.js
new file mode 100644
index 00000000000..d11af837202
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/FacetFooter.js
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import SearchSelect from '../controls/SearchSelect';
+
+type Option = { label: string, value: string };
+
+type Props = {|
+ minimumQueryLength?: number,
+ onSearch: (query: string) => Promise<Array<Option>>,
+ onSelect: (value: string) => void,
+ renderOption?: (option: Object) => React.Element<*>
+|};
+
+export default class FacetFooter extends React.PureComponent {
+ props: Props;
+
+ render() {
+ return (
+ <div className="search-navigator-facet-footer">
+ <SearchSelect autofocus={false} {...this.props} />
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/facet/FacetHeader.js b/server/sonar-web/src/main/js/components/facet/FacetHeader.js
new file mode 100644
index 00000000000..42c7b274f89
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/FacetHeader.js
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+/* eslint-disable max-len */
+import React from 'react';
+import { translate } from '../../helpers/l10n';
+
+type Props = {|
+ name: string,
+ onClear?: () => void,
+ onClick?: () => void,
+ open: boolean,
+ values?: number
+|};
+
+export default class FacetHeader extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ handleClearClick = (event: Event & { currentTarget: HTMLElement }) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ if (this.props.onClear) {
+ this.props.onClear();
+ }
+ };
+
+ handleClick = (event: Event & { currentTarget: HTMLElement }) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ if (this.props.onClick) {
+ this.props.onClick();
+ }
+ };
+
+ renderCheckbox() {
+ return (
+ <svg viewBox="0 0 1792 1792" width="10" height="10" style={{ paddingTop: 3 }}>
+ {this.props.open
+ ? <path
+ style={{ fill: 'currentColor ' }}
+ d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+ />
+ : <path
+ style={{ fill: 'currentColor ' }}
+ d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+ />}
+ </svg>
+ );
+ }
+
+ renderValueIndicator() {
+ if (this.props.open || !this.props.values) {
+ return null;
+ }
+ return (
+ <span className="spacer-left badge is-rounded">
+ {this.props.values}
+ </span>
+ );
+ }
+
+ render() {
+ const showClearButton: boolean = !!this.props.values && this.props.onClear != null;
+
+ return (
+ <div>
+ {showClearButton &&
+ <button
+ className="search-navigator-facet-header-button button-small button-red"
+ onClick={this.handleClearClick}>
+ {translate('clear')}
+ </button>}
+
+ {this.props.onClick
+ ? <a className="search-navigator-facet-header" href="#" onClick={this.handleClick}>
+ {this.renderCheckbox()} {this.props.name} {this.renderValueIndicator()}
+ </a>
+ : <span className="search-navigator-facet-header">
+ {this.props.name}
+ </span>}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/facet/FacetItem.js b/server/sonar-web/src/main/js/components/facet/FacetItem.js
new file mode 100644
index 00000000000..4ecaf7650d4
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/FacetItem.js
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+
+type Props = {|
+ active: boolean,
+ disabled: boolean,
+ halfWidth: boolean,
+ name: string | React.Element<*>,
+ onClick: string => void,
+ stat?: ?(string | React.Element<*>),
+ value: string
+|};
+
+export default class FacetItem extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ disabled: false,
+ halfWidth: false
+ };
+
+ handleClick = (event: Event & { currentTarget: HTMLElement }) => {
+ event.preventDefault();
+ this.props.onClick(this.props.value);
+ };
+
+ render() {
+ const className = classNames('facet', 'search-navigator-facet', {
+ active: this.props.active,
+ 'search-navigator-facet-half': this.props.halfWidth
+ });
+
+ return this.props.disabled
+ ? <span className={className}>
+ <span className="facet-name">
+ {this.props.name}
+ </span>
+ {this.props.stat != null &&
+ <span className="facet-stat">
+ {this.props.stat}
+ </span>}
+ </span>
+ : <a className={className} href="#" onClick={this.handleClick}>
+ <span className="facet-name">
+ {this.props.name}
+ </span>
+ {this.props.stat != null &&
+ <span className="facet-stat">
+ {this.props.stat}
+ </span>}
+ </a>;
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/facet/FacetItemsList.js b/server/sonar-web/src/main/js/components/facet/FacetItemsList.js
new file mode 100644
index 00000000000..5d36d9934e3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/FacetItemsList.js
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+
+type Props = {|
+ children?: Array<React.Element<*>>
+|};
+
+export default function FacetItemsList(props: Props) {
+ return (
+ <div className="search-navigator-facet-list">
+ {props.children}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.js b/server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.js
new file mode 100644
index 00000000000..2fb313d3f85
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.js
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import FacetBox from '../FacetBox';
+
+it('should render', () => {
+ expect(
+ shallow(
+ <FacetBox>
+ <div />
+ </FacetBox>
+ )
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.js b/server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.js
new file mode 100644
index 00000000000..4dbf1cc3ece
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.js
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import FacetFooter from '../FacetFooter';
+
+it('should render', () => {
+ expect(shallow(<FacetFooter onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.js b/server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.js
new file mode 100644
index 00000000000..aaa674c1926
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.js
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../helpers/testUtils';
+import FacetHeader from '../FacetHeader';
+
+it('should render open facet with value', () => {
+ expect(
+ shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} values={1} />)
+ ).toMatchSnapshot();
+});
+
+it('should render open facet without value', () => {
+ expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} />)).toMatchSnapshot();
+});
+
+it('should render closed facet with value', () => {
+ expect(
+ shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} values={1} />)
+ ).toMatchSnapshot();
+});
+
+it('should render closed facet without value', () => {
+ expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} />)).toMatchSnapshot();
+});
+
+it('should render without link', () => {
+ expect(shallow(<FacetHeader name="foo" open={false} />)).toMatchSnapshot();
+});
+
+it('should call onClick', () => {
+ const onClick = jest.fn();
+ const wrapper = shallow(<FacetHeader name="foo" onClick={onClick} open={false} />);
+ click(wrapper.find('a'));
+ expect(onClick).toHaveBeenCalled();
+});
+
+it('should clear', () => {
+ const onClear = jest.fn();
+ const wrapper = shallow(
+ <FacetHeader name="foo" onClear={onClear} onClick={jest.fn()} open={false} values={3} />
+ );
+ expect(wrapper).toMatchSnapshot();
+ click(wrapper.find('.button-red'));
+ expect(onClear).toHaveBeenCalled();
+});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.js b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.js
new file mode 100644
index 00000000000..2b602b29357
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.js
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../helpers/testUtils';
+import FacetItem from '../FacetItem';
+
+const renderFacetItem = (props: {}) =>
+ shallow(
+ <FacetItem active={false} name="foo" onClick={jest.fn()} stat={null} value="bar" {...props} />
+ );
+
+it('should render active', () => {
+ expect(renderFacetItem({ active: true })).toMatchSnapshot();
+});
+
+it('should render inactive', () => {
+ expect(renderFacetItem({ active: false })).toMatchSnapshot();
+});
+
+it('should render stat', () => {
+ expect(renderFacetItem({ stat: '13' })).toMatchSnapshot();
+});
+
+it('should render disabled', () => {
+ expect(renderFacetItem({ disabled: true })).toMatchSnapshot();
+});
+
+it('should render half width', () => {
+ expect(renderFacetItem({ halfWidth: true })).toMatchSnapshot();
+});
+
+it('should render effort stat', () => {
+ expect(renderFacetItem({ facetMode: 'effort', stat: '1234' })).toMatchSnapshot();
+});
+
+it('should call onClick', () => {
+ const onClick = jest.fn();
+ const wrapper = renderFacetItem({ onClick });
+ click(wrapper, { currentTarget: { dataset: { value: 'bar' } } });
+ expect(onClick).toHaveBeenCalled();
+});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.js b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.js
new file mode 100644
index 00000000000..39fc1fb4eef
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.js
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import FacetItemsList from '../FacetItemsList';
+
+it('should render', () => {
+ expect(
+ shallow(
+ <FacetItemsList>
+ <div />
+ </FacetItemsList>
+ )
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.js.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.js.snap
new file mode 100644
index 00000000000..e28d4538d46
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+ className="search-navigator-facet-box"
+>
+ <div />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.js.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.js.snap
new file mode 100644
index 00000000000..e2475bdc5dd
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+ className="search-navigator-facet-footer"
+>
+ <SearchSelect
+ autofocus={false}
+ minimumQueryLength={2}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ resetOnBlur={true}
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.js.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.js.snap
new file mode 100644
index 00000000000..8c92ac2d4ba
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.js.snap
@@ -0,0 +1,192 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should clear 1`] = `
+<div>
+ <button
+ className="search-navigator-facet-header-button button-small button-red"
+ onClick={[Function]}
+ >
+ clear
+ </button>
+ <a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}
+ >
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10"
+ >
+ <path
+ d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ }
+ />
+ </svg>
+
+ foo
+
+ <span
+ className="spacer-left badge is-rounded"
+ >
+ 3
+ </span>
+ </a>
+</div>
+`;
+
+exports[`should render closed facet with value 1`] = `
+<div>
+ <a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}
+ >
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10"
+ >
+ <path
+ d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ }
+ />
+ </svg>
+
+ foo
+
+ <span
+ className="spacer-left badge is-rounded"
+ >
+ 1
+ </span>
+ </a>
+</div>
+`;
+
+exports[`should render closed facet without value 1`] = `
+<div>
+ <a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}
+ >
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10"
+ >
+ <path
+ d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ }
+ />
+ </svg>
+
+ foo
+
+ </a>
+</div>
+`;
+
+exports[`should render open facet with value 1`] = `
+<div>
+ <a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}
+ >
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10"
+ >
+ <path
+ d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ }
+ />
+ </svg>
+
+ foo
+
+ </a>
+</div>
+`;
+
+exports[`should render open facet without value 1`] = `
+<div>
+ <a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}
+ >
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10"
+ >
+ <path
+ d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ }
+ />
+ </svg>
+
+ foo
+
+ </a>
+</div>
+`;
+
+exports[`should render without link 1`] = `
+<div>
+ <span
+ className="search-navigator-facet-header"
+ >
+ foo
+ </span>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.js.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.js.snap
new file mode 100644
index 00000000000..6aff532c59f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.js.snap
@@ -0,0 +1,93 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render active 1`] = `
+<a
+ className="facet search-navigator-facet active"
+ href="#"
+ onClick={[Function]}
+>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+</a>
+`;
+
+exports[`should render disabled 1`] = `
+<span
+ className="facet search-navigator-facet"
+>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+</span>
+`;
+
+exports[`should render effort stat 1`] = `
+<a
+ className="facet search-navigator-facet"
+ href="#"
+ onClick={[Function]}
+>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+ <span
+ className="facet-stat"
+ >
+ 1234
+ </span>
+</a>
+`;
+
+exports[`should render half width 1`] = `
+<a
+ className="facet search-navigator-facet search-navigator-facet-half"
+ href="#"
+ onClick={[Function]}
+>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+</a>
+`;
+
+exports[`should render inactive 1`] = `
+<a
+ className="facet search-navigator-facet"
+ href="#"
+ onClick={[Function]}
+>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+</a>
+`;
+
+exports[`should render stat 1`] = `
+<a
+ className="facet search-navigator-facet"
+ href="#"
+ onClick={[Function]}
+>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+ <span
+ className="facet-stat"
+ >
+ 13
+ </span>
+</a>
+`;
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.js.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.js.snap
new file mode 100644
index 00000000000..9962cfc364e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+ className="search-navigator-facet-list"
+>
+ <div />
+</div>
+`;