diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-09-06 14:33:21 +0200 |
---|---|---|
committer | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-09-11 11:28:29 +0200 |
commit | 048982bb3d8d5b2c715f95bc7818c90314e72a14 (patch) | |
tree | 1ce0d8a4051104e8790e6d0d2ea59f8d77df0e25 | |
parent | b734fdfd93438affe6c77d1da40117afd99e02c4 (diff) | |
download | sonarqube-048982bb3d8d5b2c715f95bc7818c90314e72a14.tar.gz sonarqube-048982bb3d8d5b2c715f95bc7818c90314e72a14.zip |
SONAR-4566 Identify old project on projects management page
14 files changed, 252 insertions, 24 deletions
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx index 60dd83ace5c..da819c20f4f 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx index 60f951dd32c..9ddf15b20a5 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx index ff6264dce90..af1b365a771 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx index 7b943df0908..eecfd7b4917 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx index 7b7bb09d5ae..982ef5ce35a 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx @@ -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', () => { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx index 2c0ee5f2348..66e0439de3f 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx @@ -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()} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap index b306b2fe020..d0e3ac5b4f8 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap @@ -37,6 +37,11 @@ exports[`renders 1`] = ` </Link> </td> <td + className="thin nowrap" + > + <PrivateBadge /> + </td> + <td className="nowrap" > <span @@ -46,11 +51,122 @@ 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" > <div diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap index 14bb03d1ec6..2c60880eb28 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap @@ -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]} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap index c8c1006646c..89a31381713 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap @@ -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" @@ -115,6 +115,17 @@ exports[`render qualifiers filter 1`] = ` <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" + > <Checkbox checked={false} className="link-checkbox-control" @@ -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" @@ -205,6 +216,17 @@ exports[`renders 1`] = ` <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" + > <Checkbox checked={false} className="link-checkbox-control" diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts index c78021e1fce..aa549df26c1 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts +++ b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts @@ -23,6 +23,7 @@ export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP']; export interface Project { key: string; + lastAnalysisDate?: string; name: string; qualifier: string; visibility: Visibility; diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.tsx b/server/sonar-web/src/main/js/components/controls/DateInput.tsx index 33f6c7d9cf8..6a226b1b398 100644 --- a/server/sonar-web/src/main/js/components/controls/DateInput.tsx +++ b/server/sonar-web/src/main/js/components/controls/DateInput.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/components/controls/styles.css b/server/sonar-web/src/main/js/components/controls/styles.css index 3b4e315805b..d54cab0df84 100644 --- a/server/sonar-web/src/main/js/components/controls/styles.css +++ b/server/sonar-web/src/main/js/components/controls/styles.css @@ -5,7 +5,7 @@ } .date-input-control-input { - width: 105px; + width: 130px; padding-left: 24px !important; cursor: pointer; } @@ -25,6 +25,13 @@ fill: #4b9fd5; } +.date-input-control-reset { + position: absolute; + top: 4px; + right: 4px; + border: none; +} + .boolean-toggle { display: inline-block; vertical-align: middle; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 6c022ba3d1d..0345b483fb7 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java index c6dd13c2cbd..c2031a8596b 100644 --- a/tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java +++ b/tests/src/test/java/org/sonarqube/pageobjects/ProjectsManagementPage.java @@ -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; } |