Browse Source

SONAR-7229 Create new Projects page for My Account space

tags/6.0-RC1
Stas Vilchik 8 years ago
parent
commit
59a1bedcce

+ 36
- 5
it/it-tests/src/test/java/it/user/MyAccountPageTest.java View File

@@ -20,13 +20,10 @@
package it.user;

import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
import com.sonar.orchestrator.selenium.Selenese;
import it.Category4Suite;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.*;
import org.junit.experimental.categories.Category;
import org.sonarqube.ws.client.PostRequest;
import org.sonarqube.ws.client.WsClient;
@@ -34,6 +31,7 @@ import util.QaOnly;
import util.selenium.SeleneseTest;

import static util.ItUtils.newAdminWsClient;
import static util.ItUtils.projectDir;

@Category(QaOnly.class)
public class MyAccountPageTest {
@@ -81,6 +79,24 @@ public class MyAccountPageTest {
new SeleneseTest(selenese).runOn(orchestrator);
}

@Test
public void should_display_projects() throws Exception {
// first, try on empty instance
Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("should_display_projects",
"/user/MyAccountPageTest/should_display_no_projects.html"
).build();
new SeleneseTest(selenese).runOn(orchestrator);

// then, analyze a project
analyzeProject("sample");
grantAdminPermission("account-user", "sample");

selenese = Selenese.builder().setHtmlTestsInClasspath("should_display_projects",
"/user/MyAccountPageTest/should_display_projects.html"
).build();
new SeleneseTest(selenese).runOn(orchestrator);
}

private static void createUser(String login, String name, String email) {
adminWsClient.wsConnector().call(
new PostRequest("api/users/create")
@@ -96,4 +112,19 @@ public class MyAccountPageTest {
.setParam("login", login));
}

private static void analyzeProject(String projectKey) {
SonarScanner build = SonarScanner.create(projectDir("qualityGate/xoo-sample"))
.setProjectKey(projectKey)
.setProperty("sonar.projectDescription", "Description of a project")
.setProperty("sonar.links.homepage", "http://example.com");
orchestrator.executeBuild(build);
}

private static void grantAdminPermission(String login, String projectKey) {
adminWsClient.wsConnector().call(
new PostRequest("api/permissions/add_user")
.setParam("login", login)
.setParam("projectKey", projectKey)
.setParam("permission", "admin"));
}
}

+ 55
- 0
it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_no_projects.html View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="selenium.base" href="http://localhost:49506"/>
<title>should_display_no_projects</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">should_display_no_projects</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/sessions/login</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>account-user</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>password</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/account/projects</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.account-projects</td>
<td></td>
</tr>
<tr>
<td>assertElementNotPresent</td>
<td>css=.account-projects-list</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>

+ 80
- 0
it/it-tests/src/test/resources/user/MyAccountPageTest/should_display_projects.html View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="selenium.base" href="http://localhost:49506"/>
<title>should_display_projects</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">should_display_projects</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/sessions/login</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>account-user</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>password</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/account/projects</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.account-project-card</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.account-project-name</td>
<td>*Sample*</td>
</tr>
<tr>
<td>assertText</td>
<td>css=.account-project-quality-gate</td>
<td>*Passed*</td>
</tr>
<tr>
<td>assertText</td>
<td>css=.account-project-key</td>
<td>*sample*</td>
</tr>
<tr>
<td>assertText</td>
<td>css=.account-project-description</td>
<td>*Description of a project*</td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>css=.account-project-analysis</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>css=.account-project-links a[href=&quot;http://example.com&quot;]</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>

+ 5
- 0
server/sonar-web/src/main/js/api/components.js View File

@@ -98,3 +98,8 @@ export function getProjectsWithInternalId (query) {
};
return getJSON(url, data).then(r => r.results);
}

export function getMyProjects (data) {
const url = window.baseUrl + '/api/projects/search_my_projects';
return getJSON(url, data);
}

+ 71
- 3
server/sonar-web/src/main/js/apps/account/account.css View File

@@ -7,9 +7,7 @@
background-color: #f3f3f3;
}

.account-nav {

}
.account-nav { }

.account-nav .nav-tabs {
width: 100%;
@@ -45,3 +43,73 @@
.account-bar-chart .histogram-value {
text-anchor: start;
}

.account-projects {
max-width: 600px;
}

.account-projects-list > li + li {
margin-top: 10px;
}

.account-project-side {
float: right;
margin-left: 10px;
text-align: right;
}

.account-project-analysis {
line-height: 24px;
color: #777;
font-size: 12px;
}

.account-project-card {
position: relative;
display: block;
padding: 10px 15px;
border: 1px solid #e6e6e6;
border-radius: 3px;
background-color: #fff;
}

.account-project-name {
display: inline-block;
vertical-align: top;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.account-project-name > a {
border-bottom-color: #d0d0d0;
color: #444;
}

.account-project-name > a:hover {
border-bottom-color: #cae3f2;
color: #4b9fd5;
}

.account-project-quality-gate {
display: inline-block;
vertical-align: top;
line-height: 24px;
margin-left: 8px;
}

.account-project-description {
margin-top: 6px;
line-height: 1.5;
}

.account-project-links {
margin-top: 4px;
}

.account-project-key {
margin-top: 6px;
color: #777;
font-size: 12px;
}

+ 2
- 0
server/sonar-web/src/main/js/apps/account/app.js View File

@@ -26,6 +26,7 @@ import Home from './components/Home';
import NotificationsContainer from './components/NotificationsContainer';
import Security from './components/Security';
import Issues from './components/Issues';
import ProjectsContainer from './projects/ProjectsContainer';

window.sonarqube.appStarted.then(options => {
const el = document.querySelector(options.el);
@@ -41,6 +42,7 @@ window.sonarqube.appStarted.then(options => {
<Route path="issues" component={Issues}/>
<Route path="notifications" component={NotificationsContainer}/>
<Route path="security" component={Security}/>
<Route path="projects" component={ProjectsContainer}/>

<Redirect from="/index" to="/"/>
</Route>

+ 5
- 0
server/sonar-web/src/main/js/apps/account/components/Nav.js View File

@@ -41,6 +41,11 @@ const Nav = ({ user }) => (
{translate('issues.page')}
</a>
</li>
<li>
<IndexLink to="projects" activeClassName="active">
{translate('my_account.projects')}
</IndexLink>
</li>
<li>
<IndexLink to="notifications" activeClassName="active">
{translate('my_account.notifications')}

+ 98
- 0
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.js View File

@@ -0,0 +1,98 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
import React from 'react';
import moment from 'moment';
import sortBy from 'lodash/sortBy';
import Level from '../../../components/ui/Level';
import { getComponentUrl } from '../../../helpers/urls';
import { projectType } from './propTypes';
import { translateWithParameters, translate } from '../../../helpers/l10n';

export default class ProjectCard extends React.Component {
static propTypes = {
project: projectType.isRequired
};

render () {
const { project } = this.props;
const isAnalyzed = project.lastAnalysisDate != null;
const analysisMoment = isAnalyzed && moment(project.lastAnalysisDate);
const links = sortBy(project.links, 'type');

return (
<div className="account-project-card" href="#">
<aside className="account-project-side">
{isAnalyzed ? (
<div className="account-project-analysis"
title={analysisMoment.format('LLL')}>
{translateWithParameters(
'my_account.projects.analyzed_x',
analysisMoment.fromNow()
)}
</div>
) : (
<div className="account-project-analysis">
{translate('my_account.projects.never_analyzed')}
</div>
)}

{links.length > 0 && (
<div className="account-project-links">
<ul className="list-inline">
{links.map(link => (
<li key={link.type}>
<a
className="link-with-icon"
href={link.href}
title={link.name}
target="_blank"
rel="nofollow">
<i className={`icon-color-link icon-${link.type}`}/>
</a>
</li>
))}
</ul>
</div>
)}
</aside>

<h3 className="account-project-name">
<a href={getComponentUrl(project.key)}>
{project.name}
</a>
</h3>

{project.qualityGate != null && (
<div className="account-project-quality-gate">
<Level level={project.qualityGate}/>
</div>
)}

<div className="account-project-key">{project.key}</div>

{!!project.description && (
<div className="account-project-description">
{project.description}
</div>
)}
</div>
);
}
}

+ 70
- 0
server/sonar-web/src/main/js/apps/account/projects/Projects.js View File

@@ -0,0 +1,70 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
import React from 'react';
import ProjectCard from './ProjectCard';
import ProjectsSearch from './ProjectsSearch';
import ListFooter from '../../../components/controls/ListFooter';
import { projectsListType } from './propTypes';
import { translate } from '../../../helpers/l10n';

export default class Projects extends React.Component {
static propTypes = {
projects: projectsListType.isRequired,
total: React.PropTypes.number.isRequired,
loading: React.PropTypes.bool.isRequired,
search: React.PropTypes.func.isRequired,
loadMore: React.PropTypes.func.isRequired
};

render () {
const { projects } = this.props;

return (
<div className="page page-limited account-projects">
<ProjectsSearch
onSearch={this.props.search}/>

{projects.length === 0 && (
<div className="js-no-results">
{translate('no_results')}
</div>
)}

{projects.length > 0 && (
<ul className="account-projects-list">
{projects.map(project => (
<li key={project.key}>
<ProjectCard project={project}/>
</li>
))}
</ul>
)}

{projects.length > 0 && (
<ListFooter
count={projects.length}
total={this.props.total}
ready={!this.props.loading}
loadMore={this.props.loadMore}/>
)}
</div>
);
}
}

+ 95
- 0
server/sonar-web/src/main/js/apps/account/projects/ProjectsContainer.js View File

@@ -0,0 +1,95 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
import React from 'react';
import Projects from './Projects';
import { getMyProjects } from '../../../api/components';

export default class ProjectsContainer extends React.Component {
state = {
loading: true,
page: 1,
query: ''
};

componentWillMount () {
this.loadMore = this.loadMore.bind(this);
this.search = this.search.bind(this);
document.querySelector('html').classList.add('dashboard-page');
}

componentDidMount () {
this.mounted = true;
this.loadProjects();
}

componentWillUnmount () {
this.mounted = false;
document.querySelector('html').classList.remove('dashboard-page');
}

loadProjects (page = this.state.page, query = this.state.query) {
this.setState({ loading: true });
const data = { ps: 20 }; // FIXME
if (page > 1) {
data.p = page;
}
if (query) {
data.q = query;
}
return getMyProjects(data).then(r => {
const projects = page > 1 ?
[...this.state.projects, ...r.projects] : r.projects;
this.setState({
projects,
query,
loading: false,
page: r.paging.pageIndex,
total: r.paging.total
});
});
}

loadMore () {
return this.loadProjects(this.state.page + 1);
}

search (query) {
return this.loadProjects(1, query);
}

render () {
if (this.state.projects == null) {
return (
<div className="text-center">
<i className="spinner spinner-margin"/>
</div>
);
}

return (
<Projects
projects={this.state.projects}
total={this.state.total}
loading={this.state.loading}
loadMore={this.loadMore}
search={this.search}/>
);
}
}

+ 65
- 0
server/sonar-web/src/main/js/apps/account/projects/ProjectsSearch.js View File

@@ -0,0 +1,65 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
import React from 'react';
import debounce from 'lodash/debounce';
import { translate, translateWithParameters } from '../../../helpers/l10n';

export default class ProjectsSearch extends React.Component {
static propTypes = {
onSearch: React.PropTypes.func.isRequired
};

componentWillMount () {
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.onSearch = debounce(this.props.onSearch, 250);
}

handleChange () {
const { value } = this.refs.input;
if (value.length > 2 || value.length === 0) {
this.onSearch(value);
}
}

handleSubmit (e) {
e.preventDefault();
this.handleChange();
}

render () {
return (
<div className="big-spacer-bottom">
<form onSubmit={this.handleSubmit}>
<input
ref="input"
type="search"
className="input-large"
placeholder={translate('search_verb')}
onChange={this.handleChange}/>
<span className="note spacer-left">
{translateWithParameters(
'my_account.projects.x_characters_min', 3)}
</span>
</form>
</div>
);
}
}

+ 94
- 0
server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js View File

@@ -0,0 +1,94 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { expect } from 'chai';
import ProjectCard from '../ProjectCard';
import Level from '../../../../components/ui/Level';

const BASE = { id: 'id', key: 'key', name: 'name', links: [] };

describe('My Account :: ProjectCard', () => {
it('should render key and name', () => {
const project = { ...BASE };
const output = shallow(
<ProjectCard project={project}/>
);
expect(output.find('.account-project-key').text()).to.equal('key');
expect(output.find('.account-project-name').text()).to.equal('name');
});

it('should render description', () => {
const project = { ...BASE, description: 'bla' };
const output = shallow(
<ProjectCard project={project}/>
);
expect(output.find('.account-project-description').text()).to.equal('bla');
});

it('should not render optional fields', () => {
const project = { ...BASE };
const output = shallow(
<ProjectCard project={project}/>
);
expect(output.find('.account-project-description')).to.have.length(0);
expect(output.find('.account-project-quality-gate')).to.have.length(0);
expect(output.find('.account-project-links')).to.have.length(0);
});

it('should render analysis date', () => {
const project = { ...BASE, lastAnalysisDate: '2016-05-17' };
const output = shallow(
<ProjectCard project={project}/>
);
expect(output.find('.account-project-analysis').text())
.to.contain('my_account.projects.analyzed_x');
});

it('should not render analysis date', () => {
const project = { ...BASE };
const output = shallow(
<ProjectCard project={project}/>
);
expect(output.find('.account-project-analysis').text())
.to.contain('my_account.projects.never_analyzed');
});

it('should render quality gate status', () => {
const project = { ...BASE, qualityGate: 'ERROR' };
const output = shallow(
<ProjectCard project={project}/>
);
expect(
output.find('.account-project-quality-gate').find(Level).prop('level')
).to.equal('ERROR');
});

it('should render links', () => {
const project = {
...BASE,
links: [{ name: 'n', type: 't', href: 'h' }]
};
const output = shallow(
<ProjectCard project={project}/>
);
expect(output.find('.account-project-links').find('li')).to.have.length(1);
});
});

+ 83
- 0
server/sonar-web/src/main/js/apps/account/projects/__tests__/Projects-test.js View File

@@ -0,0 +1,83 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { expect } from 'chai';
import sinon from 'sinon';
import Projects from '../Projects';
import ProjectCard from '../ProjectCard';
import ListFooter from '../../../../components/controls/ListFooter';

describe('My Account :: Projects', () => {
it('should render list of ProjectCards', () => {
const projects = [
{ id: 'id1', key: 'key1', name: 'name1', links: [] },
{ id: 'id2', key: 'key2', name: 'name2', links: [] }
];

const output = shallow(
<Projects
projects={projects}
total={5}
loading={false}
search={() => true}
loadMore={() => true}/>
);

expect(output.find(ProjectCard)).to.have.length(2);
});

it('should render ListFooter', () => {
const projects = [
{ id: 'id1', key: 'key1', name: 'name1', links: [] },
{ id: 'id2', key: 'key2', name: 'name2', links: [] }
];
const loadMore = sinon.stub().throws();

const footer = shallow(
<Projects
projects={projects}
total={5}
loading={false}
search={() => true}
loadMore={loadMore}/>
).find(ListFooter);

expect(footer).to.have.length(1);
expect(footer.prop('count')).to.equal(2);
expect(footer.prop('total')).to.equal(5);
expect(footer.prop('loadMore')).to.equal(loadMore);
});

it('should render when no results', () => {
const output = shallow(
<Projects
projects={[]}
total={0}
loading={false}
search={() => true}
loadMore={() => true}/>
);

expect(output.find('.js-no-results')).to.have.length(1);
expect(output.find(ProjectCard)).to.have.length(0);
expect(output.find(ListFooter)).to.have.length(0);
});
});

+ 34
- 0
server/sonar-web/src/main/js/apps/account/projects/propTypes.js View File

@@ -0,0 +1,34 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
import React from 'react';

const { shape, string, array, arrayOf } = React.PropTypes;

export const projectType = shape({
id: string.isRequired,
key: string.isRequired,
name: string.isRequired,
lastAnalysisDate: string,
description: string,
links: array.isRequired,
qualityGate: string
});

export const projectsListType = arrayOf(projectType);

+ 4
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -2122,6 +2122,10 @@ my_account.issue_widget.by_project=My Issues by Project
my_account.issue_widget.by_severity=My Issues by Severity
my_account.to_fix=To Fix
my_account.to_review=To Review
my_account.projects=Projects
my_account.projects.analyzed_x=Analyzed {0}
my_account.projects.never_analyzed=Never analyzed
my_account.projects.x_characters_min=({0} characters min)




Loading…
Cancel
Save