]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8693 Do not allow to filter portfolios or applications by status on management...
authorStas Vilchik <stas.vilchik@sonarsource.com>
Wed, 6 Sep 2017 09:30:15 +0000 (11:30 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Sep 2017 09:28:29 +0000 (11:28 +0200)
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/utils.ts
server/sonar-web/src/main/less/components/react-select.less
server/sonar-web/src/main/less/init/links.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 88ac5426f91cfec7f49e23aa92ef36dc87a1d667..60dd83ace5c50e1e9926689ab2e52eead2275b64 100644 (file)
@@ -25,7 +25,7 @@ import Search from './Search';
 import Projects from './Projects';
 import CreateProjectForm from './CreateProjectForm';
 import ListFooter from '../../components/controls/ListFooter';
-import { PAGE_SIZE, Type, Project } from './utils';
+import { PAGE_SIZE, Project } from './utils';
 import { getComponents, getProvisioned } from '../../api/components';
 import { Organization } from '../../app/types';
 import { translate } from '../../helpers/l10n';
@@ -41,12 +41,12 @@ interface State {
   createProjectForm: boolean;
   page: number;
   projects: Project[];
+  provisioned: boolean;
   qualifiers: string;
   query: string;
   ready: boolean;
   selection: string[];
   total: number;
-  type: Type;
 }
 
 export default class App extends React.PureComponent<Props, State> {
@@ -58,11 +58,11 @@ export default class App extends React.PureComponent<Props, State> {
       createProjectForm: false,
       ready: false,
       projects: [],
+      provisioned: false,
       total: 0,
       page: 1,
       query: '',
       qualifiers: 'TRK',
-      type: Type.All,
       selection: []
     };
     this.requestProjects = debounce(this.requestProjects, 250);
@@ -84,16 +84,8 @@ export default class App extends React.PureComponent<Props, State> {
     q: this.state.query ? this.state.query : undefined
   });
 
-  requestProjects = () => {
-    switch (this.state.type) {
-      case Type.All:
-        this.requestAllProjects();
-        break;
-      case Type.Provisioned:
-        this.requestProvisioned();
-        break;
-    }
-  };
+  requestProjects = () =>
+    this.state.provisioned ? this.requestProvisioned() : this.requestAllProjects();
 
   requestProvisioned = () => {
     const data = this.getFilters();
@@ -134,16 +126,23 @@ export default class App extends React.PureComponent<Props, State> {
     this.setState({ ready: false, page: 1, query, selection: [] }, this.requestProjects);
   };
 
-  onTypeChanged = (newType: Type) => {
+  onProvisionedChanged = (provisioned: boolean) => {
     this.setState(
-      { ready: false, page: 1, query: '', type: newType, qualifiers: 'TRK', selection: [] },
+      { ready: false, page: 1, query: '', provisioned, qualifiers: 'TRK', selection: [] },
       this.requestProjects
     );
   };
 
   onQualifierChanged = (newQualifier: string) => {
     this.setState(
-      { ready: false, page: 1, query: '', type: Type.All, qualifiers: newQualifier, selection: [] },
+      {
+        ready: false,
+        page: 1,
+        provisioned: false,
+        query: '',
+        qualifiers: newQualifier,
+        selection: []
+      },
       this.requestProjects
     );
   };
@@ -188,14 +187,21 @@ export default class App extends React.PureComponent<Props, State> {
         />
 
         <Search
-          {...this.props}
-          {...this.state}
           onAllSelected={this.onAllSelected}
           onAllDeselected={this.onAllDeselected}
           onDeleteProjects={this.requestProjects}
+          onProvisionedChanged={this.onProvisionedChanged}
           onQualifierChanged={this.onQualifierChanged}
           onSearch={this.onSearch}
-          onTypeChanged={this.onTypeChanged}
+          organization={this.props.organization}
+          projects={this.state.projects}
+          provisioned={this.state.provisioned}
+          qualifiers={this.state.qualifiers}
+          query={this.state.query}
+          ready={this.state.ready}
+          selection={this.state.selection}
+          topLevelQualifiers={this.props.topLevelQualifiers}
+          total={this.state.total}
         />
 
         <Projects
index a5f967fd04a18de75565cc4302c65d082927ef5d..60c55b7f5fc620d696bb28bde9f471f1d684ae47 100644 (file)
@@ -20,7 +20,6 @@
 import * as React from 'react';
 import Modal from 'react-modal';
 import * as Select from 'react-select';
-import { Type } from './utils';
 import {
   getPermissionTemplates,
   PermissionTemplate,
@@ -32,11 +31,11 @@ import { translate, translateWithParameters } from '../../helpers/l10n';
 export interface Props {
   onClose: () => void;
   organization: string;
+  provisioned: boolean;
   qualifier: string;
   query: string;
   selection: string[];
   total: number;
-  type: Type;
 }
 
 interface State {
index 69f0f958080b2c8784b322383f6b996e46efd20d..7b943df0908973e2354e198bfc187c7f5f81d0d3 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import * as Select from 'react-select';
 import { sortBy } from 'lodash';
 import BulkApplyTemplateModal from './BulkApplyTemplateModal';
 import DeleteModal from './DeleteModal';
-import { Type, QUALIFIERS_ORDER } from './utils';
+import { QUALIFIERS_ORDER } from './utils';
 import { Project } from './utils';
 import { Organization } from '../../app/types';
-import RadioToggle from '../../components/controls/RadioToggle';
 import Checkbox from '../../components/controls/Checkbox';
 import { translate } from '../../helpers/l10n';
+import QualifierIcon from '../../components/shared/QualifierIcon';
+import Tooltip from '../../components/controls/Tooltip';
 
 export interface Props {
   onAllDeselected: () => void;
   onAllSelected: () => void;
   onDeleteProjects: () => void;
+  onProvisionedChanged: (provisioned: boolean) => void;
   onQualifierChanged: (qualifier: string) => void;
   onSearch: (query: string) => void;
-  onTypeChanged: (type: Type) => void;
   organization: Organization;
   projects: Project[];
+  provisioned: boolean;
   qualifiers: string;
   query: string;
   ready: boolean;
   selection: any[];
   topLevelQualifiers: string[];
   total: number;
-  type: Type;
 }
 
 interface State {
@@ -66,15 +68,11 @@ export default class Search extends React.PureComponent<Props, State> {
     this.props.onSearch(q);
   };
 
-  getTypeOptions = () => [
-    { value: Type.All, label: 'All' },
-    { value: Type.Provisioned, label: 'Provisioned' }
-  ];
-
   getQualifierOptions = () => {
-    const options = this.props.topLevelQualifiers.map(q => {
-      return { value: q, label: translate('qualifiers', q) };
-    });
+    const options = this.props.topLevelQualifiers.map(q => ({
+      label: translate('qualifiers', q),
+      value: q
+    }));
     return sortBy(options, option => QUALIFIERS_ORDER.indexOf(option.value));
   };
 
@@ -111,6 +109,8 @@ export default class Search extends React.PureComponent<Props, State> {
     this.setState({ bulkApplyTemplateModal: false });
   };
 
+  handleQualifierChange = ({ value }: { value: string }) => this.props.onQualifierChanged(value);
+
   renderCheckbox = () => {
     const isAllChecked =
       this.props.projects.length > 0 && this.props.selection.length === this.props.projects.length;
@@ -119,9 +119,22 @@ export default class Search extends React.PureComponent<Props, State> {
       this.props.selection.length > 0 &&
       this.props.selection.length < this.props.projects.length;
     const checked = isAllChecked || thirdState;
-    return <Checkbox checked={checked} thirdState={thirdState} onCheck={this.onCheck} />;
+    return (
+      <Checkbox
+        checked={checked}
+        id="projects-selection"
+        thirdState={thirdState}
+        onCheck={this.onCheck}
+      />
+    );
   };
 
+  renderQualifierOption = (option: { label: string; value: string }) =>
+    <span>
+      <QualifierIcon className="little-spacer-right" qualifier={option.value} />
+      {option.label}
+    </span>;
+
   renderQualifierFilter = () => {
     const options = this.getQualifierOptions();
     if (options.length < 2) {
@@ -129,16 +142,40 @@ export default class Search extends React.PureComponent<Props, State> {
     }
     return (
       <td className="thin nowrap text-middle">
-        <RadioToggle
+        <Select
+          className="input-medium"
+          clearable={false}
+          disabled={!this.props.ready}
+          optionRenderer={this.renderQualifierOption}
           options={this.getQualifierOptions()}
           value={this.props.qualifiers}
+          valueRenderer={this.renderQualifierOption}
           name="projects-qualifier"
-          onCheck={this.props.onQualifierChanged}
+          onChange={this.handleQualifierChange}
+          searchable={false}
         />
       </td>
     );
   };
 
+  renderTypeFilter = () =>
+    this.props.qualifiers === 'TRK'
+      ? <td className="thin nowrap text-middle">
+          <Checkbox
+            className="link-checkbox-control"
+            checked={this.props.provisioned}
+            id="projects-provisioned"
+            onCheck={this.props.onProvisionedChanged}>
+            <span className="little-spacer-left">
+              {translate('provisioning.only_provisioned')}
+              <Tooltip overlay={translate('provisioning.only_provisioned.tooltip')}>
+                <i className="spacer-left icon-help" />
+              </Tooltip>
+            </span>
+          </Checkbox>
+        </td>
+      : null;
+
   render() {
     const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0;
     return (
@@ -150,14 +187,7 @@ export default class Search extends React.PureComponent<Props, State> {
                 {this.props.ready ? this.renderCheckbox() : <i className="spinner" />}
               </td>
               {this.renderQualifierFilter()}
-              <td className="thin nowrap text-middle">
-                <RadioToggle
-                  options={this.getTypeOptions()}
-                  value={this.props.type}
-                  name="projects-type"
-                  onCheck={this.props.onTypeChanged}
-                />
-              </td>
+              {this.renderTypeFilter()}
               <td className="text-middle">
                 <form onSubmit={this.onSubmit} className="search-box">
                   <button className="search-box-submit button-clean">
@@ -194,11 +224,11 @@ export default class Search extends React.PureComponent<Props, State> {
           <BulkApplyTemplateModal
             onClose={this.closeBulkApplyTemplateModal}
             organization={this.props.organization.key}
+            provisioned={this.props.provisioned}
             qualifier={this.props.qualifiers}
             query={this.props.query}
             selection={this.props.selection}
             total={this.props.total}
-            type={this.props.type}
           />}
 
         {this.state.deleteModal &&
index ae2cfdcee0efe2c15b1c965de8238256d614c06f..07339957c4cc768a3513081b29b56f858b210227 100644 (file)
@@ -23,6 +23,13 @@ jest.mock('lodash', () => {
   return lodash;
 });
 
+// actual version breaks `mount`
+jest.mock('rc-tooltip', () => ({
+  default: function Tooltip() {
+    return null;
+  }
+}));
+
 jest.mock('../../../api/components', () => ({
   getComponents: jest.fn(),
   getProvisioned: jest.fn(() => Promise.resolve({ paging: { total: 0 }, projects: [] }))
@@ -31,7 +38,6 @@ jest.mock('../../../api/components', () => ({
 import * as React from 'react';
 import { mount } from 'enzyme';
 import App, { Props } from '../App';
-import { Type } from '../utils';
 
 const getComponents = require('../../../api/components').getComponents as jest.Mock<any>;
 const getProvisioned = require('../../../api/components').getProvisioned as jest.Mock<any>;
@@ -57,15 +63,15 @@ it('fetches all projects on mount', () => {
   expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'TRK' });
 });
 
-it('changes type', () => {
+it('selects provisioned', () => {
   const wrapper = mountRender();
-  wrapper.find('Search').prop<Function>('onTypeChanged')(Type.Provisioned);
+  wrapper.find('Search').prop<Function>('onProvisionedChanged')(true);
   expect(getProvisioned).lastCalledWith(defaultSearchParameters);
 });
 
-it('changes qualifier and resets type', () => {
+it('changes qualifier and resets provisioned', () => {
   const wrapper = mountRender();
-  wrapper.setState({ type: Type.Provisioned });
+  wrapper.setState({ provisioned: true });
   wrapper.find('Search').prop<Function>('onQualifierChanged')('VW');
   expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'VW' });
 });
index 1c8399e6eda18528b05c4535a1cb3f7795de31dc..6f11a5d48daefdb28986b676ee2aa49613bff87a 100644 (file)
@@ -26,7 +26,6 @@ jest.mock('../../../api/permissions', () => ({
 import * as React from 'react';
 import { mount, shallow } from 'enzyme';
 import BulkApplyTemplateModal, { Props } from '../BulkApplyTemplateModal';
-import { Type } from '../utils';
 import { click } from '../../../helpers/testUtils';
 
 const applyTemplateToProject = require('../../../api/permissions')
@@ -117,11 +116,11 @@ function render(props?: { [P in keyof Props]?: Props[P] }) {
     <BulkApplyTemplateModal
       onClose={jest.fn()}
       organization="org"
+      provisioned={true}
       qualifier="TRK"
       query="bla"
       selection={[]}
       total={17}
-      type={Type.All}
       {...props}
     />
   );
index 950b78e1624ae808d10718ab10fd1e847b8e7a0c..2c0ee5f234881377212e23e51f0c197e99cc018c 100644 (file)
@@ -20,7 +20,6 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import Search, { Props } from '../Search';
-import { Type } from '../utils';
 import { change, click } from '../../../helpers/testUtils';
 
 const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
@@ -36,15 +35,22 @@ it('render qualifiers filter', () => {
 it('updates qualifier', () => {
   const onQualifierChanged = jest.fn();
   const wrapper = shallowRender({ onQualifierChanged, topLevelQualifiers: ['TRK', 'VW', 'APP'] });
-  wrapper.find('RadioToggle[name="projects-qualifier"]').prop<Function>('onCheck')('VW');
+  wrapper.find('Select[name="projects-qualifier"]').prop<Function>('onChange')({ value: 'VW' });
   expect(onQualifierChanged).toBeCalledWith('VW');
 });
 
-it('updates type', () => {
-  const onTypeChanged = jest.fn();
-  const wrapper = shallowRender({ onTypeChanged });
-  wrapper.find('RadioToggle[name="projects-type"]').prop<Function>('onCheck')(Type.Provisioned);
-  expect(onTypeChanged).toBeCalledWith(Type.Provisioned);
+it('selects provisioned', () => {
+  const onProvisionedChanged = jest.fn();
+  const wrapper = shallowRender({ onProvisionedChanged });
+  wrapper.find('Checkbox[id="projects-provisioned"]').prop<Function>('onCheck')(true);
+  expect(onProvisionedChanged).toBeCalledWith(true);
+});
+
+it('does not render provisioned filter for portfolios', () => {
+  const wrapper = shallowRender();
+  expect(wrapper.find('Checkbox[id="projects-provisioned"]').exists()).toBeTruthy();
+  wrapper.setProps({ qualifiers: 'VW' });
+  expect(wrapper.find('Checkbox[id="projects-provisioned"]').exists()).toBeFalsy();
 });
 
 it('searches', () => {
@@ -59,10 +65,10 @@ it('checks all or none projects', () => {
   const onAllSelected = jest.fn();
   const wrapper = shallowRender({ onAllDeselected, onAllSelected });
 
-  wrapper.find('Checkbox').prop<Function>('onCheck')(true);
+  wrapper.find('Checkbox[id="projects-selection"]').prop<Function>('onCheck')(true);
   expect(onAllSelected).toBeCalled();
 
-  wrapper.find('Checkbox').prop<Function>('onCheck')(false);
+  wrapper.find('Checkbox[id="projects-selection"]').prop<Function>('onCheck')(false);
   expect(onAllDeselected).toBeCalled();
 });
 
@@ -89,18 +95,18 @@ function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
       onAllDeselected={jest.fn()}
       onAllSelected={jest.fn()}
       onDeleteProjects={jest.fn()}
+      onProvisionedChanged={jest.fn()}
       onQualifierChanged={jest.fn()}
       onSearch={jest.fn()}
-      onTypeChanged={jest.fn()}
       organization={organization}
       projects={[]}
+      provisioned={false}
       qualifiers="TRK"
       query=""
       ready={true}
       selection={[]}
       topLevelQualifiers={['TRK']}
       total={0}
-      type={Type.All}
       {...props}
     />
   );
index eb9c3bd28cabc3c590125eb6a8a2ce06c28f22bc..c8c1006646c5828c8f7e716c922f8b8395a17652 100644 (file)
@@ -4,11 +4,11 @@ exports[`bulk applies permission template 1`] = `
 <BulkApplyTemplateModal
   onClose={[Function]}
   organization="org"
+  provisioned={false}
   qualifier="TRK"
   query=""
   selection={Array []}
   total={0}
-  type="ALL"
 />
 `;
 
@@ -41,6 +41,7 @@ exports[`render qualifiers filter 1`] = `
         >
           <Checkbox
             checked={false}
+            id="projects-selection"
             onCheck={[Function]}
             thirdState={false}
           />
@@ -48,10 +49,40 @@ exports[`render qualifiers filter 1`] = `
         <td
           className="thin nowrap text-middle"
         >
-          <RadioToggle
+          <Select
+            addLabelText="Add \\"{label}\\"?"
+            arrowRenderer={[Function]}
+            autosize={true}
+            backspaceRemoves={true}
+            backspaceToRemoveMessage="Press backspace to remove {label}"
+            className="input-medium"
+            clearAllText="Clear all"
+            clearRenderer={[Function]}
+            clearValueText="Clear value"
+            clearable={false}
+            deleteRemoves={true}
+            delimiter=","
             disabled={false}
+            escapeClearsValue={true}
+            filterOptions={[Function]}
+            ignoreAccents={true}
+            ignoreCase={true}
+            inputProps={Object {}}
+            isLoading={false}
+            joinValues={false}
+            labelKey="label"
+            matchPos="any"
+            matchProp="any"
+            menuBuffer={0}
+            menuRenderer={[Function]}
+            multi={false}
             name="projects-qualifier"
-            onCheck={[Function]}
+            noResultsText="No results found"
+            onBlurResetsInput={true}
+            onChange={[Function]}
+            onCloseResetsInput={true}
+            optionComponent={[Function]}
+            optionRenderer={[Function]}
             options={
               Array [
                 Object {
@@ -68,30 +99,43 @@ exports[`render qualifiers filter 1`] = `
                 },
               ]
             }
+            pageSize={5}
+            placeholder="Select..."
+            required={false}
+            scrollMenuIntoView={true}
+            searchable={false}
+            simpleValue={false}
+            tabSelectsValue={true}
             value="TRK"
+            valueComponent={[Function]}
+            valueKey="value"
+            valueRenderer={[Function]}
           />
         </td>
         <td
           className="thin nowrap text-middle"
         >
-          <RadioToggle
-            disabled={false}
-            name="projects-type"
+          <Checkbox
+            checked={false}
+            className="link-checkbox-control"
+            id="projects-provisioned"
             onCheck={[Function]}
-            options={
-              Array [
-                Object {
-                  "label": "All",
-                  "value": "ALL",
-                },
-                Object {
-                  "label": "Provisioned",
-                  "value": "PROVISIONED",
-                },
-              ]
-            }
-            value="ALL"
-          />
+            thirdState={false}
+          >
+            <span
+              className="little-spacer-left"
+            >
+              provisioning.only_provisioned
+              <Tooltip
+                overlay="provisioning.only_provisioned.tooltip"
+                placement="bottom"
+              >
+                <i
+                  className="spacer-left icon-help"
+                />
+              </Tooltip>
+            </span>
+          </Checkbox>
         </td>
         <td
           className="text-middle"
@@ -153,6 +197,7 @@ exports[`renders 1`] = `
         >
           <Checkbox
             checked={false}
+            id="projects-selection"
             onCheck={[Function]}
             thirdState={false}
           />
@@ -160,24 +205,27 @@ exports[`renders 1`] = `
         <td
           className="thin nowrap text-middle"
         >
-          <RadioToggle
-            disabled={false}
-            name="projects-type"
+          <Checkbox
+            checked={false}
+            className="link-checkbox-control"
+            id="projects-provisioned"
             onCheck={[Function]}
-            options={
-              Array [
-                Object {
-                  "label": "All",
-                  "value": "ALL",
-                },
-                Object {
-                  "label": "Provisioned",
-                  "value": "PROVISIONED",
-                },
-              ]
-            }
-            value="ALL"
-          />
+            thirdState={false}
+          >
+            <span
+              className="little-spacer-left"
+            >
+              provisioning.only_provisioned
+              <Tooltip
+                overlay="provisioning.only_provisioned.tooltip"
+                placement="bottom"
+              >
+                <i
+                  className="spacer-left icon-help"
+                />
+              </Tooltip>
+            </span>
+          </Checkbox>
         </td>
         <td
           className="text-middle"
index 8f2b119bfabadd487a46f41d74f2935fd913cbb4..c78021e1fceb4b2338b9d24559398bf7832ac9fc 100644 (file)
  */
 export const PAGE_SIZE = 50;
 
-export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP', 'DEV'];
-
-export enum Type {
-  All = 'ALL',
-  Provisioned = 'PROVISIONED'
-}
+export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP'];
 
 export interface Project {
   key: string;
index b14f3c147ca0d08f6da4239f6243cac0dbbbadc7..f03a781f4eabc89f043c3e082739524b9d808b68 100644 (file)
 
 .Select-value svg,
 .Select-value [class^="icon-"] {
-  padding-top: 4px;
+  padding-top: 5px;
 }
 
 .Select-value img {
index 42e595415c7037492952911599a0c88747561588..e84469eba160bc385f078be980bdea1c15f4156d 100644 (file)
@@ -83,6 +83,12 @@ a {
   }
 }
 
+.link-checkbox-control {
+  display: inline-block;
+  padding: 4px 0 5px;
+  line-height: 16px;
+}
+
 a.active-link,
 .link-active {
   .link-no-underline;
index 916420b80b927852ac62198757e90a6ff1b8c676..6c022ba3d1dd04898140200a716da50448fade10 100644 (file)
@@ -1997,6 +1997,8 @@ provisioning.missing.name=Name is missing
 provisioning.no_analysis=No analysis has been performed since creation. The only available section is the configuration.
 provisioning.no_analysis.delete=Either you should retry to analyze the project, or simply {0}.
 provisioning.no_analysis.delete_it=delete it
+provisioning.only_provisioned=Only Provisioned
+provisioning.only_provisioned.tooltip=Provisioned projects are projects that have been created, but have not been analyzed yet.
 
 
 #------------------------------------------------------------------------------