]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4566 Identify old project on projects management page
authorStas Vilchik <stas.vilchik@sonarsource.com>
Wed, 6 Sep 2017 12:33:21 +0000 (14:33 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Sep 2017 09:28:29 +0000 (11:28 +0200)
14 files changed:
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap
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/js/components/controls/DateInput.tsx
server/sonar-web/src/main/js/components/controls/styles.css
sonar-core/src/main/resources/org/sonar/l10n/core.properties
tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java

index 60dd83ace5c50e1e9926689ab2e52eead2275b64..da819c20f4f41406661261007cc463308e42b161 100644 (file)
@@ -38,6 +38,7 @@ export interface Props {
 }
 
 interface State {
+  analyzedBefore?: string;
   createProjectForm: boolean;
   page: number;
   projects: Project[];
@@ -78,6 +79,7 @@ export default class App extends React.PureComponent<Props, State> {
   }
 
   getFilters = () => ({
+    analyzedBefore: this.state.analyzedBefore,
     organization: this.props.organization.key,
     p: this.state.page !== 1 ? this.state.page : undefined,
     ps: PAGE_SIZE,
@@ -147,6 +149,9 @@ export default class App extends React.PureComponent<Props, State> {
     );
   };
 
+  handleDateChanged = (analyzedBefore?: string) =>
+    this.setState({ ready: false, page: 1, analyzedBefore }, this.requestProjects);
+
   onProjectSelected = (project: string) => {
     const newSelection = uniq([...this.state.selection, project]);
     this.setState({ selection: newSelection });
@@ -187,8 +192,10 @@ export default class App extends React.PureComponent<Props, State> {
         />
 
         <Search
+          analyzedBefore={this.state.analyzedBefore}
           onAllSelected={this.onAllSelected}
           onAllDeselected={this.onAllDeselected}
+          onDateChanged={this.handleDateChanged}
           onDeleteProjects={this.requestProjects}
           onProvisionedChanged={this.onProvisionedChanged}
           onQualifierChanged={this.onQualifierChanged}
index 60f951dd32c02e1efd8b3f549f5c1d0154a88411..9ddf15b20a55d80ca163223e4dad0baa2c3503a8 100644 (file)
@@ -25,6 +25,7 @@ import Checkbox from '../../components/controls/Checkbox';
 import QualifierIcon from '../../components/shared/QualifierIcon';
 import { translate } from '../../helpers/l10n';
 import { getComponentPermissionsUrl } from '../../helpers/urls';
+import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';
 
 interface Props {
   onApplyTemplateClick: (project: Project) => void;
@@ -61,14 +62,20 @@ export default class ProjectRow extends React.PureComponent<Props> {
           </Link>
         </td>
 
+        <td className="thin nowrap">
+          {project.visibility === Visibility.Private && <PrivateBadge />}
+        </td>
+
         <td className="nowrap">
           <span className="note">
             {project.key}
           </span>
         </td>
 
-        <td className="width-20">
-          {project.visibility === Visibility.Private && <PrivateBadge />}
+        <td className="thin nowrap text-right">
+          {project.lastAnalysisDate
+            ? <DateTooltipFormatter date={project.lastAnalysisDate} />
+            : <span className="note">—</span>}
         </td>
 
         <td className="thin nowrap">
index ff6264dce90638138b16b4a49dbf229c1674b104..af1b365a771d6c3c200bcffcc91f799b6c43fe15 100644 (file)
@@ -51,6 +51,16 @@ export default class Projects extends React.PureComponent<Props> {
       <table
         className={classNames('data', 'zebra', { 'new-loading': !this.props.ready })}
         id="projects-management-page-projects">
+        <thead>
+          <tr>
+            <th />
+            <th>Name</th>
+            <th />
+            <th>Key</th>
+            <th className="thin nowrap text-right">Last Analysis</th>
+            <th />
+          </tr>
+        </thead>
         <tbody>
           {this.props.projects.map(project =>
             <ProjectRow
index 7b943df0908973e2354e198bfc187c7f5f81d0d3..eecfd7b491705024f4b1e0824c77afd666ceef06 100644 (file)
@@ -29,10 +29,13 @@ import Checkbox from '../../components/controls/Checkbox';
 import { translate } from '../../helpers/l10n';
 import QualifierIcon from '../../components/shared/QualifierIcon';
 import Tooltip from '../../components/controls/Tooltip';
+import DateInput from '../../components/controls/DateInput';
 
 export interface Props {
+  analyzedBefore?: string;
   onAllDeselected: () => void;
   onAllSelected: () => void;
+  onDateChanged: (analyzedBefore?: string) => void;
   onDeleteProjects: () => void;
   onProvisionedChanged: (provisioned: boolean) => void;
   onQualifierChanged: (qualifier: string) => void;
@@ -176,10 +179,24 @@ export default class Search extends React.PureComponent<Props, State> {
         </td>
       : null;
 
+  renderDateFilter = () => {
+    return (
+      <td className="thin nowrap text-middle">
+        <DateInput
+          inputClassName="input-medium"
+          name="analyzed-before"
+          onChange={this.props.onDateChanged}
+          placeholder={translate('analyzed_before')}
+          value={this.props.analyzedBefore}
+        />
+      </td>
+    );
+  };
+
   render() {
     const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0;
     return (
-      <div className="panel panel-vertical bordered-bottom spacer-bottom">
+      <div className="big-spacer-bottom">
         <table className="data">
           <tbody>
             <tr>
@@ -187,6 +204,7 @@ export default class Search extends React.PureComponent<Props, State> {
                 {this.props.ready ? this.renderCheckbox() : <i className="spinner" />}
               </td>
               {this.renderQualifierFilter()}
+              {this.renderDateFilter()}
               {this.renderTypeFilter()}
               <td className="text-middle">
                 <form onSubmit={this.onSubmit} className="search-box">
index 7b7bb09d5ae96a3fa018b785a44b043aefb9619d..982ef5ce35a3267c76887a9caeaff51bc2824d00 100644 (file)
@@ -32,6 +32,9 @@ const project = {
 
 it('renders', () => {
   expect(shallowRender()).toMatchSnapshot();
+  expect(
+    shallowRender({ project: { ...project, lastAnalysisDate: '2017-04-08T00:00:00.000Z' } })
+  ).toMatchSnapshot();
 });
 
 it('checks project', () => {
index 2c0ee5f234881377212e23e51f0c197e99cc018c..66e0439de3f5bcae50b4c2755497296c98e15e94 100644 (file)
@@ -53,6 +53,17 @@ it('does not render provisioned filter for portfolios', () => {
   expect(wrapper.find('Checkbox[id="projects-provisioned"]').exists()).toBeFalsy();
 });
 
+it('updates analysis date', () => {
+  const onDateChanged = jest.fn();
+  const wrapper = shallowRender({ onDateChanged });
+
+  wrapper.find('DateInput').prop<Function>('onChange')('2017-04-08T00:00:00.000Z');
+  expect(onDateChanged).toBeCalledWith('2017-04-08T00:00:00.000Z');
+
+  wrapper.find('DateInput').prop<Function>('onChange')(undefined);
+  expect(onDateChanged).toBeCalledWith(undefined);
+});
+
 it('searches', () => {
   const onSearch = jest.fn();
   const wrapper = shallowRender({ onSearch });
@@ -94,6 +105,7 @@ function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
     <Search
       onAllDeselected={jest.fn()}
       onAllSelected={jest.fn()}
+      onDateChanged={jest.fn()}
       onDeleteProjects={jest.fn()}
       onProvisionedChanged={jest.fn()}
       onQualifierChanged={jest.fn()}
index b306b2fe0206f2dedf8f1841b4d122b7e02dd2ba..d0e3ac5b4f86529a9827eb9cb1aa70814130c35d 100644 (file)
@@ -36,6 +36,11 @@ exports[`renders 1`] = `
       </span>
     </Link>
   </td>
+  <td
+    className="thin nowrap"
+  >
+    <PrivateBadge />
+  </td>
   <td
     className="nowrap"
   >
@@ -46,10 +51,121 @@ exports[`renders 1`] = `
     </span>
   </td>
   <td
-    className="width-20"
+    className="thin nowrap text-right"
+  >
+    <span
+      className="note"
+    >
+      —
+    </span>
+  </td>
+  <td
+    className="thin nowrap"
+  >
+    <div
+      className="dropdown"
+    >
+      <button
+        className="dropdown-toggle"
+        data-toggle="dropdown"
+      >
+        actions
+         
+        <i
+          className="icon-dropdown"
+        />
+      </button>
+      <ul
+        className="dropdown-menu dropdown-menu-right"
+      >
+        <li>
+          <Link
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to={
+              Object {
+                "pathname": "/project_roles",
+                "query": Object {
+                  "id": "project",
+                },
+              }
+            }
+          >
+            edit_permissions
+          </Link>
+        </li>
+        <li>
+          <a
+            className="js-apply-template"
+            href="#"
+            onClick={[Function]}
+          >
+            projects_role.apply_template
+          </a>
+        </li>
+      </ul>
+    </div>
+  </td>
+</tr>
+`;
+
+exports[`renders 2`] = `
+<tr>
+  <td
+    className="thin"
+  >
+    <Checkbox
+      checked={true}
+      onCheck={[Function]}
+      thirdState={false}
+    />
+  </td>
+  <td
+    className="nowrap"
+  >
+    <Link
+      className="link-with-icon"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/dashboard",
+          "query": Object {
+            "id": "project",
+          },
+        }
+      }
+    >
+      <QualifierIcon
+        qualifier="TRK"
+      />
+       
+      <span>
+        Project
+      </span>
+    </Link>
+  </td>
+  <td
+    className="thin nowrap"
   >
     <PrivateBadge />
   </td>
+  <td
+    className="nowrap"
+  >
+    <span
+      className="note"
+    >
+      project
+    </span>
+  </td>
+  <td
+    className="thin nowrap text-right"
+  >
+    <DateTooltipFormatter
+      date="2017-04-08T00:00:00.000Z"
+    />
+  </td>
   <td
     className="thin nowrap"
   >
index 14bb03d1ec694c2ea7f7bfde6330d233e80e0308..2c60880eb28eb6f42a10757619b739b7651e8fab 100644 (file)
@@ -5,6 +5,24 @@ exports[`renders list of projects 1`] = `
   className="data zebra new-loading"
   id="projects-management-page-projects"
 >
+  <thead>
+    <tr>
+      <th />
+      <th>
+        Name
+      </th>
+      <th />
+      <th>
+        Key
+      </th>
+      <th
+        className="thin nowrap text-right"
+      >
+        Last Analysis
+      </th>
+      <th />
+    </tr>
+  </thead>
   <tbody>
     <ProjectRow
       onApplyTemplateClick={[Function]}
index c8c1006646c5828c8f7e716c922f8b8395a17652..89a31381713c42f1d2a12529fcb60f5df94e18c7 100644 (file)
@@ -29,7 +29,7 @@ exports[`deletes projects 1`] = `
 
 exports[`render qualifiers filter 1`] = `
 <div
-  className="panel panel-vertical bordered-bottom spacer-bottom"
+  className="big-spacer-bottom"
 >
   <table
     className="data"
@@ -112,6 +112,17 @@ exports[`render qualifiers filter 1`] = `
             valueRenderer={[Function]}
           />
         </td>
+        <td
+          className="thin nowrap text-middle"
+        >
+          <DateInput
+            format="yy-mm-dd"
+            inputClassName="input-medium"
+            name="analyzed-before"
+            onChange={[Function]}
+            placeholder="last_analysis_before"
+          />
+        </td>
         <td
           className="thin nowrap text-middle"
         >
@@ -185,7 +196,7 @@ exports[`render qualifiers filter 1`] = `
 
 exports[`renders 1`] = `
 <div
-  className="panel panel-vertical bordered-bottom spacer-bottom"
+  className="big-spacer-bottom"
 >
   <table
     className="data"
@@ -202,6 +213,17 @@ exports[`renders 1`] = `
             thirdState={false}
           />
         </td>
+        <td
+          className="thin nowrap text-middle"
+        >
+          <DateInput
+            format="yy-mm-dd"
+            inputClassName="input-medium"
+            name="analyzed-before"
+            onChange={[Function]}
+            placeholder="last_analysis_before"
+          />
+        </td>
         <td
           className="thin nowrap text-middle"
         >
index c78021e1fceb4b2338b9d24559398bf7832ac9fc..aa549df26c1d69d045b122723a56236195daa4f2 100644 (file)
@@ -23,6 +23,7 @@ export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP'];
 
 export interface Project {
   key: string;
+  lastAnalysisDate?: string;
   name: string;
   qualifier: string;
   visibility: Visibility;
index 33f6c7d9cf85701387d30ad333d8876fb2076055..6a226b1b398929bafe2176ba9c160c04333bf3f0 100644 (file)
@@ -22,21 +22,22 @@ import * as React from 'react';
 import * as classNames from 'classnames';
 import { pick } from 'lodash';
 import './styles.css';
+import CloseIcon from '../icons-components/CloseIcon';
 
 interface Props {
   className?: string;
-  value?: string;
   format?: string;
+  inputClassName?: string;
   name: string;
+  onChange: (value?: string) => void;
   placeholder: string;
-  onChange: (value: string) => void;
+  value?: string;
 }
 
 export default class DateInput extends React.PureComponent<Props> {
   input: HTMLInputElement;
 
   static defaultProps = {
-    value: '',
     format: 'yy-mm-dd'
   };
 
@@ -44,23 +45,23 @@ export default class DateInput extends React.PureComponent<Props> {
     this.attachDatePicker();
   }
 
-  componentWillReceiveProps(nextProps: Props) {
-    if (nextProps.value != null && this.input) {
-      this.input.value = nextProps.value;
-    }
-  }
-
-  handleChange() {
+  handleChange = () => {
     const { value } = this.input;
     this.props.onChange(value);
-  }
+  };
+
+  handleResetClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onChange(undefined);
+  };
 
   attachDatePicker() {
     const opts = {
       dateFormat: this.props.format,
       changeMonth: true,
       changeYear: true,
-      onSelect: this.handleChange.bind(this)
+      onSelect: this.handleChange
     };
 
     if ($.fn && ($.fn as any).datepicker && this.input) {
@@ -74,11 +75,12 @@ export default class DateInput extends React.PureComponent<Props> {
     return (
       <span className={classNames('date-input-control', this.props.className)}>
         <input
-          className="date-input-control-input"
-          ref={node => (this.input = node as HTMLInputElement)}
-          type="text"
-          defaultValue={this.props.value}
+          className={classNames('date-input-control-input', this.props.inputClassName)}
+          onChange={this.handleChange}
           readOnly={true}
+          ref={node => (this.input = node!)}
+          type="text"
+          value={this.props.value || ''}
           {...inputProps}
         />
         <span className="date-input-control-icon">
@@ -86,6 +88,10 @@ export default class DateInput extends React.PureComponent<Props> {
             <path d="M5.5 6h2v2h-2V6zm3 0h2v2h-2V6zm3 0h2v2h-2V6zm-9 6h2v2h-2v-2zm3 0h2v2h-2v-2zm3 0h2v2h-2v-2zm-3-3h2v2h-2V9zm3 0h2v2h-2V9zm3 0h2v2h-2V9zm-9 0h2v2h-2V9zm11-9v1h-2V0h-7v1h-2V0h-2v16h15V0h-2zm1 15h-13V4h13v11z" />
           </svg>
         </span>
+        {this.props.value != undefined &&
+          <a className="date-input-control-reset" href="#" onClick={this.handleResetClick}>
+            <CloseIcon className="" />
+          </a>}
       </span>
     );
   }
index 3b4e315805b9d66c4fcd28d357a460de2a6e19da..d54cab0df8427f22056e715b37af7c50406dbc6b 100644 (file)
@@ -5,7 +5,7 @@
 }
 
 .date-input-control-input {
-  width: 105px;
+  width: 130px;
   padding-left: 24px !important;
   cursor: pointer;
 }
   fill: #4b9fd5;
 }
 
+.date-input-control-reset {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  border: none;
+}
+
 .boolean-toggle {
   display: inline-block;
   vertical-align: middle;
index 6c022ba3d1dd04898140200a716da50448fade10..0345b483fb77b4bb279eba50dbda0cab1eef3be0 100644 (file)
@@ -225,6 +225,7 @@ added_since_previous_version_detailed=Added since previous version ({0})
 added_since_version=Added since version {0}
 all_violations=All violations
 all_issues=All issues
+analyzed_before=Analyzed before
 and_worse=and worse
 are_you_sure=Are you sure?
 assigned_to=Assigned to
index c6dd13c2cbd0252991a95d80512f06e1f8a78618..c2031a8596bca7885fe57fc62d952665afc07fab 100644 (file)
@@ -32,7 +32,7 @@ public class ProjectsManagementPage {
   }
 
   public ProjectsManagementPage shouldHaveProjectsCount(int count) {
-    $$("#projects-management-page-projects tr").shouldHaveSize(count);
+    $$("#projects-management-page-projects tbody tr").shouldHaveSize(count);
     return this;
   }