Browse Source

SONAR-8451 Run js app outside of ruby container

tags/6.3-RC1
Stas Vilchik 7 years ago
parent
commit
f1976a3f56
100 changed files with 1904 additions and 894 deletions
  1. 2
    0
      it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java
  2. 1
    0
      it/it-tests/src/test/java/it/issue/IssueSearchTest.java
  3. 1
    1
      it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java
  4. 1
    0
      it/it-tests/src/test/java/it/projectEvent/EventTest.java
  5. 2
    0
      it/it-tests/src/test/java/it/qualityGate/QualityGateNotificationTest.java
  6. 2
    0
      it/it-tests/src/test/java/it/qualityGate/QualityGateUiTest.java
  7. 3
    0
      it/it-tests/src/test/java/it/uiExtension/UiExtensionsTest.java
  8. 1
    0
      it/it-tests/src/test/java/it/user/LocalAuthenticationTest.java
  9. 4
    2
      it/it-tests/src/test/java/it/user/SsoAuthenticationTest.java
  10. 11
    1
      it/it-tests/src/test/resources/updateCenter/installed-plugins.html
  11. 0
    11
      it/it-tests/src/test/resources/user/LocalAuthenticationTest/force-authentication.html
  12. 0
    5
      it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html
  13. 0
    2
      server/sonar-web/config/webpack/webpack.config.base.js
  14. 1
    1
      server/sonar-web/public/index.html
  15. 7
    0
      server/sonar-web/src/main/js/api/auth.js
  16. 5
    0
      server/sonar-web/src/main/js/api/settings.js
  17. 2
    2
      server/sonar-web/src/main/js/api/system.js
  18. 25
    19
      server/sonar-web/src/main/js/app/components/AdminContainer.js
  19. 36
    6
      server/sonar-web/src/main/js/app/components/App.js
  20. 15
    16
      server/sonar-web/src/main/js/app/components/GlobalContainer.js
  21. 81
    0
      server/sonar-web/src/main/js/app/components/GlobalFooter.js
  22. 19
    0
      server/sonar-web/src/main/js/app/components/GlobalLoading.css
  23. 33
    0
      server/sonar-web/src/main/js/app/components/GlobalLoading.js
  24. 49
    0
      server/sonar-web/src/main/js/app/components/Landing.js
  25. 49
    0
      server/sonar-web/src/main/js/app/components/LocalizationContainer.js
  26. 52
    0
      server/sonar-web/src/main/js/app/components/MigrationContainer.js
  27. 33
    0
      server/sonar-web/src/main/js/app/components/NotFound.js
  28. 64
    0
      server/sonar-web/src/main/js/app/components/ProjectContainer.js
  29. 55
    0
      server/sonar-web/src/main/js/app/components/SimpleContainer.js
  30. 0
    91
      server/sonar-web/src/main/js/app/components/nav/app.js
  31. 16
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
  32. 25
    20
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
  33. 9
    3
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
  34. 8
    3
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js
  35. 62
    58
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
  36. 0
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js
  37. 6
    8
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js
  38. 38
    28
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
  39. 26
    13
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.js
  40. 27
    21
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
  41. 28
    22
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js
  42. 23
    23
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js
  43. 3
    2
      server/sonar-web/src/main/js/app/components/nav/global/SearchView.js
  44. 0
    0
      server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelpView.js
  45. 134
    0
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js
  46. 0
    133
      server/sonar-web/src/main/js/app/components/nav/settings/settings-nav.js
  47. 12
    2
      server/sonar-web/src/main/js/app/index.js
  48. 75
    0
      server/sonar-web/src/main/js/app/store/appState/duck.js
  49. 48
    3
      server/sonar-web/src/main/js/app/store/rootActions.js
  50. 10
    0
      server/sonar-web/src/main/js/app/store/rootReducer.js
  51. 3
    3
      server/sonar-web/src/main/js/app/store/users/actions.js
  52. 2
    2
      server/sonar-web/src/main/js/app/store/users/reducer.js
  53. 9
    11
      server/sonar-web/src/main/js/app/utils/getCurrentUserFromStore.js
  54. 35
    0
      server/sonar-web/src/main/js/app/utils/getHistory.js
  55. 33
    0
      server/sonar-web/src/main/js/app/utils/getStore.js
  56. 36
    0
      server/sonar-web/src/main/js/app/utils/handleRequiredAuthentication.js
  57. 36
    0
      server/sonar-web/src/main/js/app/utils/handleRequiredAuthorization.js
  58. 4
    3
      server/sonar-web/src/main/js/app/utils/handleRequiredMigration.js
  59. 0
    69
      server/sonar-web/src/main/js/app/utils/isCurrentPathKnown.js
  60. 7
    0
      server/sonar-web/src/main/js/app/utils/startAjaxMonitoring.js
  61. 0
    69
      server/sonar-web/src/main/js/app/utils/startApp.js
  62. 72
    53
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  63. 31
    13
      server/sonar-web/src/main/js/apps/about/components/AboutApp.js
  64. 0
    10
      server/sonar-web/src/main/js/apps/account/components/Account.js
  65. 10
    3
      server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
  66. 9
    3
      server/sonar-web/src/main/js/apps/code/components/App.js
  67. 11
    2
      server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js
  68. 15
    13
      server/sonar-web/src/main/js/apps/component-issues/init.js
  69. 5
    6
      server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js
  70. 12
    7
      server/sonar-web/src/main/js/apps/component/components/App.js
  71. 28
    0
      server/sonar-web/src/main/js/apps/component/routes.js
  72. 10
    10
      server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js
  73. 1
    1
      server/sonar-web/src/main/js/apps/issues/HeaderView.js
  74. 14
    2
      server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js
  75. 4
    4
      server/sonar-web/src/main/js/apps/issues/init.js
  76. 9
    1
      server/sonar-web/src/main/js/apps/overview/components/App.js
  77. 6
    26
      server/sonar-web/src/main/js/apps/overview/components/AppContainer.js
  78. 5
    5
      server/sonar-web/src/main/js/apps/overview/meta/Meta.js
  79. 13
    2
      server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js
  80. 9
    19
      server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js
  81. 12
    2
      server/sonar-web/src/main/js/apps/permissions/project/components/App.js
  82. 2
    1
      server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js
  83. 9
    1
      server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js
  84. 3
    2
      server/sonar-web/src/main/js/apps/project-admin/key/Key.js
  85. 3
    2
      server/sonar-web/src/main/js/apps/project-admin/links/Links.js
  86. 3
    2
      server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js
  87. 7
    2
      server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
  88. 5
    15
      server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js
  89. 0
    4
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
  90. 1
    1
      server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.js
  91. 6
    8
      server/sonar-web/src/main/js/apps/quality-profiles/components/App.js
  92. 2
    1
      server/sonar-web/src/main/js/apps/quality-profiles/components/AppContainer.js
  93. 102
    0
      server/sonar-web/src/main/js/apps/sessions/components/LoginForm.js
  94. 78
    0
      server/sonar-web/src/main/js/apps/sessions/components/LoginFormContainer.js
  95. 42
    0
      server/sonar-web/src/main/js/apps/sessions/components/Logout.js
  96. 49
    0
      server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.js
  97. 31
    0
      server/sonar-web/src/main/js/apps/sessions/routes.js
  98. 6
    18
      server/sonar-web/src/main/js/apps/settings/components/AppContainer.js
  99. 10
    2
      server/sonar-web/src/main/js/apps/settings/components/EmailForm.js
  100. 0
    0
      server/sonar-web/src/main/js/apps/settings/store/values/reducer.js

+ 2
- 0
it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java View File

@@ -26,6 +26,7 @@ import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Test;
import org.sonar.wsclient.issue.BulkChangeQuery;
import org.sonar.wsclient.issue.Issue;
@@ -42,6 +43,7 @@ import static util.ItUtils.runProjectAnalysis;
import static util.ItUtils.setServerProperty;
import static util.selenium.Selenese.runSelenese;

@Ignore("notifications page is not available yet")
public class IssueNotificationsTest extends AbstractIssueTest {

private final static String PROJECT_KEY = "sample";

+ 1
- 0
it/it-tests/src/test/java/it/issue/IssueSearchTest.java View File

@@ -310,6 +310,7 @@ public class IssueSearchTest extends AbstractIssueTest {
}

@Test
@Ignore("bulk change form is not available yet")
public void bulk_change() {
runSelenese(ORCHESTRATOR, "/issue/IssueSearchTest/bulk_change.html");
}

+ 1
- 1
it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java View File

@@ -127,8 +127,8 @@ public class ProjectAdministrationTest {
}

// SONAR-4203
@Ignore("UUID column added in Events page")
@Test
@Ignore("history page is not available yet")
public void delete_version_of_multimodule_project() {
GregorianCalendar today = new GregorianCalendar();
SonarScanner build = SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"))

+ 1
- 0
it/it-tests/src/test/java/it/projectEvent/EventTest.java View File

@@ -39,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.projectDir;
import static util.selenium.Selenese.runSelenese;

@Ignore("history page is not available yet")
public class EventTest {

@ClassRule

+ 2
- 0
it/it-tests/src/test/java/it/qualityGate/QualityGateNotificationTest.java View File

@@ -28,6 +28,7 @@ import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Test;
import org.sonar.wsclient.Sonar;
import org.sonar.wsclient.qualitygate.NewCondition;
@@ -78,6 +79,7 @@ public class QualityGateNotificationTest {
}

@Test
@Ignore("notifications page is not available yet")
public void status_on_metric_variation_and_send_notifications() throws Exception {
Wiser smtpServer = new Wiser(0);
try {

+ 2
- 0
it/it-tests/src/test/java/it/qualityGate/QualityGateUiTest.java View File

@@ -27,6 +27,7 @@ import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Test;
import org.sonar.wsclient.qualitygate.NewCondition;
import org.sonar.wsclient.qualitygate.QualityGate;
@@ -69,6 +70,7 @@ public class QualityGateUiTest {
* SONAR-3326
*/
@Test
@Ignore("history page is not available yet")
public void display_alerts_correctly_in_history_page() {
QualityGateClient qgClient = qgClient();
QualityGate qGate = qgClient.create("AlertsForHistory");

+ 3
- 0
it/it-tests/src/test/java/it/uiExtension/UiExtensionsTest.java View File

@@ -23,6 +23,7 @@ import com.sonar.orchestrator.Orchestrator;
import it.Category4Suite;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Test;

import static util.selenium.Selenese.runSelenese;
@@ -48,6 +49,7 @@ public class UiExtensionsTest {
* SONAR-2376
*/
@Test
@Ignore("page extensions are not reimplemented yet")
public void test_page_decoration() {
runSelenese(orchestrator, "/uiExtension/UiExtensionsTest/page-decoration.html");
}
@@ -56,6 +58,7 @@ public class UiExtensionsTest {
* SONAR-4173
*/
@Test
@Ignore("page extensions are not reimplemented yet")
public void test_resource_configuration_extension() {
runSelenese(orchestrator, "/uiExtension/UiExtensionsTest/resource-configuration-extension.html");
}

+ 1
- 0
it/it-tests/src/test/java/it/user/LocalAuthenticationTest.java View File

@@ -183,6 +183,7 @@ public class LocalAuthenticationTest {
}

@Test
@Ignore("signing up will be dropped: SONAR-7762")
public void allow_users_to_sign_up() throws IOException {
setServerProperty(ORCHESTRATOR, "sonar.allowUsersToSignUp", "true");


+ 4
- 2
it/it-tests/src/test/java/it/user/SsoAuthenticationTest.java View File

@@ -20,6 +20,7 @@
package it.user;

import com.sonar.orchestrator.Orchestrator;
import java.net.URLEncoder;
import java.util.List;
import javax.annotation.Nullable;
import okhttp3.Response;
@@ -30,6 +31,7 @@ import org.junit.ClassRule;
import org.junit.Test;
import util.user.UserRule;

import static com.google.common.base.Charsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.call;

@@ -126,7 +128,7 @@ public class SsoAuthenticationTest {
Response response = doCall("invalid login $", null, null, null);

assertThat(response.code()).isEqualTo(200);
assertThat(response.body().string()).contains("You're not authorized to access this page. Please contact the administrator");
assertThat(response.request().url().toString()).contains("sessions/unauthorized");

List<String> logsLines = FileUtils.readLines(orchestrator.getServer().getWebLogs(), Charsets.UTF_8);
assertThat(logsLines).doesNotContain("org.sonar.server.exceptions.BadRequestException: user.bad_login");
@@ -141,7 +143,7 @@ public class SsoAuthenticationTest {

String expectedError = "You can't sign up because email 'tester@email.com' is already used by an existing user. This means that you probably already registered with another account";
assertThat(response.code()).isEqualTo(200);
assertThat(response.body().string()).contains(expectedError);
assertThat(response.request().url().toString()).contains(URLEncoder.encode(expectedError, UTF_8.name()));
assertThat(FileUtils.readLines(orchestrator.getServer().getWebLogs(), Charsets.UTF_8)).doesNotContain(expectedError);
}


+ 11
- 1
it/it-tests/src/test/resources/updateCenter/installed-plugins.html View File

@@ -15,7 +15,12 @@
</tr>
<tr>
<td>open</td>
<td>/updatecenter</td>
<td>/sessions/login</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>name=commit</td>
<td></td>
</tr>
<tr>
@@ -38,6 +43,11 @@
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/updatecenter</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>

+ 0
- 11
it/it-tests/src/test/resources/user/LocalAuthenticationTest/force-authentication.html View File

@@ -23,11 +23,6 @@
<td>/</td>
<td></td>
</tr>
<tr>
<td>assertLocation</td>
<td>*/sessions/new</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
@@ -63,12 +58,6 @@
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>assertLocation</td>
<td>*/sessions/new</td>
<td></td>
</tr>

</tbody>
</table>
</body>

+ 0
- 5
it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html View File

@@ -17,11 +17,6 @@
<td>/settings</td>
<td></td>
</tr>
<tr>
<td>assertLocation</td>
<td>*/sessions/new</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>

+ 0
- 2
server/sonar-web/config/webpack/webpack.config.base.js View File

@@ -20,8 +20,6 @@ module.exports = {
'handlebars/runtime'
],

'sonar': './src/main/js/libs/sonar.js',

'app': './src/main/js/app/index.js'
},
output: {

+ 1
- 1
server/sonar-web/public/index.html View File

@@ -3,7 +3,7 @@
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="%WEB_CONTEXT%/favicon.ico" rel="shortcut icon" type="image/x-icon">
<link href="%WEB_CONTEXT%/images/favicon.ico" rel="shortcut icon" type="image/x-icon">
<% for (var css in htmlWebpackPlugin.files.css) { %>
<link href="%WEB_CONTEXT%/<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
<% } %>

+ 7
- 0
server/sonar-web/src/main/js/api/auth.js View File

@@ -36,3 +36,10 @@ export const login = (login, password) => (
.submit()
.then(basicCheckStatus)
);

export const logout = () => (
request('/api/authentication/logout')
.setMethod('POST')
.submit()
.then(basicCheckStatus)
);

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

@@ -93,3 +93,8 @@ export function getServerId () {
export function generateServerId (organization, ip) {
return postJSON('/api/server_id/generate', { organization, ip });
}

// TODO replace with /api/settings
export const getSettingValue = key => (
getJSON(`/api/properties/${key}`).then(r => r[0] ? r[0].value : null)
);

+ 2
- 2
server/sonar-web/src/main/js/api/system.js View File

@@ -30,7 +30,7 @@ export function getSystemInfo () {
return getJSON(url);
}

export function getStatus () {
export function getSystemStatus () {
const url = '/api/system/status';
return getJSON(url);
}
@@ -44,7 +44,7 @@ const POLLING_INTERVAL = 2000;

function pollStatus (cb) {
setTimeout(() => {
getStatus()
getSystemStatus()
.then(r => {
if (r.status === 'UP') {
cb();

server/sonar-web/src/main/js/apps/about/components/LoginSection.js → server/sonar-web/src/main/js/app/components/AdminContainer.js View File

@@ -18,30 +18,36 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { Link } from 'react-router';
import OAuthProvider from './OAuthProvider';
import IconLock from './IconLock';
import { connect } from 'react-redux';
import SettingsNav from './nav/settings/SettingsNav';
import { getCurrentUser } from '../store/rootReducer';
import { isUserAdmin } from '../../helpers/users';

export default class LoginSection extends React.Component {
render () {
const { authProviders } = window.sonarqube;
class AdminContainer extends React.Component {
componentDidMount () {
if (!isUserAdmin(this.props.currentUser)) {
// workaround cyclic dependencies
const handleRequiredAuthorization = require('../utils/handleRequiredAuthorization').default;
handleRequiredAuthorization();
}
}

const loginWithSonarQubeLabel = authProviders.length ? 'Log in with SonarQube' : 'Log in';
render () {
if (!isUserAdmin(this.props.currentUser)) {
return null;
}

return (
<div id="about-login">
<div className="about-page-auth-providers big-spacer-top">
{authProviders.map(provider => (
<OAuthProvider key={provider.key} provider={provider}/>
))}

<Link to={{ pathname: '/about', query: { login: null } }}
className="oauth-provider oauth-provider-sonarqube">
<IconLock/>
<span>{loginWithSonarQubeLabel}</span>
</Link>
</div>
<div>
<SettingsNav/>
{this.props.children}
</div>
);
}
}

const mapStateToProps = state => ({
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(AdminContainer);

+ 36
- 6
server/sonar-web/src/main/js/app/components/App.js View File

@@ -17,27 +17,57 @@
* 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 { connect } from 'react-redux';
import GlobalLoading from './GlobalLoading';
import { fetchCurrentUser } from '../store/users/actions';
import { fetchLanguages } from '../store/rootActions';
import { fetchLanguages, fetchAppState } from '../store/rootActions';

class App extends React.Component {
mounted: bool;

static propTypes = {
fetchCurrentUser: React.PropTypes.func.isRequired
fetchAppState: React.PropTypes.func.isRequired,
fetchCurrentUser: React.PropTypes.func.isRequired,
fetchLanguages: React.PropTypes.func.isRequired,
children: React.PropTypes.element.isRequired
};

state = {
loading: true
};

finishLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};

componentDidMount () {
this.props.fetchCurrentUser();
this.props.fetchLanguages();
this.mounted = true;

this.props.fetchCurrentUser()
.then(this.props.fetchAppState)
.then(this.finishLoading)
.then(this.props.fetchLanguages)
.catch(this.finishLoading);
}

componentWillUnmount () {
this.mounted = false;
}

render () {
if (this.state.loading) {
return <GlobalLoading/>;
}

return this.props.children;
}
}

export default connect(
() => ({}),
{ fetchCurrentUser, fetchLanguages }
null,
{ fetchAppState, fetchCurrentUser, fetchLanguages }
)(App);

server/sonar-web/src/main/js/app/components/nav/links-mixin.js → server/sonar-web/src/main/js/app/components/GlobalContainer.js View File

@@ -17,24 +17,23 @@
* 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';

export default {
activeLink(url) {
return window.location.pathname.indexOf(window.baseUrl + url) === 0 ? 'active' : null;
},

renderLink(url, title, highlightUrl = url) {
const fullUrl = window.baseUrl + url;
const isActive = typeof highlightUrl === 'string' ?
window.location.pathname.indexOf(window.baseUrl + highlightUrl) === 0 :
highlightUrl(fullUrl);
import GlobalNav from './nav/global/GlobalNav';
import GlobalFooter from './GlobalFooter';
import GlobalMessagesContainer from './GlobalMessagesContainer';

export default class GlobalContainer extends React.Component {
render () {
return (
<li key={url} className={classNames({ 'active': isActive })}>
<a href={fullUrl}>{title}</a>
</li>
<div className="global-container">
<div className="page-wrapper page-wrapper-global" id="container">
<GlobalNav/>
<GlobalMessagesContainer/>
{this.props.children}
</div>
<GlobalFooter/>
</div>
);
}
};
}

+ 81
- 0
server/sonar-web/src/main/js/app/components/GlobalFooter.js View File

@@ -0,0 +1,81 @@
/*
* 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.
*/
// @flow
import React from 'react';
import { connect } from 'react-redux';
import { getAppState } from '../store/rootReducer';

class GlobalFooter extends React.Component {
render () {
const { sonarqubeVersion, productionDatabase } = this.props;

return (
<div id="footer" className="page-footer page-container">
{!productionDatabase && (
<div className="alert alert-danger">
<p className="big" id="evaluation_warning">
Embedded database should be used for evaluation purpose only
</p>
<p>
The embedded database will not scale, it will not support upgrading to newer versions of SonarQube,
and there is no support for migrating your data out of it into a different database engine.
</p>
</div>
)}

<div>
This application is based on
{' '}
<a href="http://www.sonarqube.org/" title="SonarQube&trade;">SonarQube&trade;</a>
{' '}
but is <strong>not</strong> an official version provided by
{' '}
<a href="http://www.sonarsource.com" title="SonarSource SA">SonarSource SA</a>.
</div>


<div>
Version {sonarqubeVersion}
{' - '}
<a href="http://www.gnu.org/licenses/lgpl-3.0.txt">LGPL v3</a>
{' - '}
<a href="http://www.sonarqube.org">Community</a>
{' - '}
<a href="http://www.sonarqube.org/documentation">Documentation</a>
{' - '}
<a href="http://www.sonarqube.org/support">Get Support</a>
{' - '}
<a href="http://redirect.sonarsource.com/doc/plugin-library.html">Plugins</a>
{' - '}
<a href={window.baseUrl + '/web_api'}>Web API</a>
{' - '}
<a href={window.baseUrl + '/about'}>About</a>
</div>
</div>
);
}
}

const mapStateToProps = state => ({
sonarqubeVersion: getAppState(state).version,
productionDatabase: getAppState(state).productionDatabase
});

export default connect(mapStateToProps)(GlobalFooter);

+ 19
- 0
server/sonar-web/src/main/js/app/components/GlobalLoading.css View File

@@ -0,0 +1,19 @@
.global-loading {
width: 300px;
margin: 200px auto 0;
white-space: nowrap;
}

.global-loading-spinner {
vertical-align: middle;
width: 80px;
height: 80px;
}

.global-loading-text {
display: inline-block;
vertical-align: middle;
margin-left: 30px;
font-size: 36px;
font-weight: 300;
}

+ 33
- 0
server/sonar-web/src/main/js/app/components/GlobalLoading.js View File

@@ -0,0 +1,33 @@
/*
* 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.
*/
// @flow
import React from 'react';
import './GlobalLoading.css';

export default class GlobalLoading extends React.Component {
render () {
return (
<div className="global-loading">
<i className="spinner global-loading-spinner"/>
<span className="global-loading-text">Loading...</span>
</div>
);
}
}

+ 49
- 0
server/sonar-web/src/main/js/app/components/Landing.js View File

@@ -0,0 +1,49 @@
/*
* 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.
*/
// @flow
import React from 'react';
import { withRouter } from 'react-router';
import { connect } from 'react-redux';
import { getCurrentUser } from '../store/rootReducer';

class Landing extends React.Component {
static propTypes = {
currentUser: React.PropTypes.oneOfType([React.PropTypes.bool, React.PropTypes.object]).isRequired
};

componentDidMount () {
const { currentUser, router } = this.props;
if (currentUser.isLoggedIn) {
router.replace('/projects/favorite');
} else {
router.replace('/about');
}
}

render () {
return null;
}
}

const mapStateToProps = state => ({
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(withRouter(Landing));

+ 49
- 0
server/sonar-web/src/main/js/app/components/LocalizationContainer.js View File

@@ -0,0 +1,49 @@
/*
* 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.
*/
// @flow
import React from 'react';
import { requestMessages } from '../../helpers/l10n';

export default class LocalizationContainer extends React.Component {
mounted: bool;

state = {
loading: true
};

finishLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};

componentDidMount () {
this.mounted = true;
requestMessages().then(this.finishLoading, this.finishLoading);
}

componentWillUnmount () {
this.mounted = false;
}

render () {
return this.state.loading ? null : this.props.children;
}
}

+ 52
- 0
server/sonar-web/src/main/js/app/components/MigrationContainer.js View File

@@ -0,0 +1,52 @@
/*
* 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.
*/
// @flow
import React from 'react';
import { getSystemStatus } from '../../api/system';

export default class MigrationContainer extends React.Component {
static propTypes = {
children: React.PropTypes.element.isRequired
};

state = {
loading: true
};

componentDidMount () {
getSystemStatus().then(r => {
if (r.status === 'UP') {
this.setState({ loading: false });
} else {
// workaround cyclic dependencies
const handleRequiredMigration = require('../utils/handleRequiredMigration').default;
handleRequiredMigration();
}
});
}

render () {
if (this.state.loading) {
return null;
}

return this.props.children;
}
}

+ 33
- 0
server/sonar-web/src/main/js/app/components/NotFound.js View File

@@ -0,0 +1,33 @@
/*
* 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 SimpleContainer from './SimpleContainer';

export default class NotFound extends React.Component {
render () {
return (
<SimpleContainer>
<h2 className="big-spacer-bottom">The page you were looking for does not exist.</h2>
<p className="spacer-bottom">You may have mistyped the address or the page may have moved.</p>
<p><a href={window.baseUrl + '/'}>Go back to the homepage</a></p>
</SimpleContainer>
);
}
}

+ 64
- 0
server/sonar-web/src/main/js/app/components/ProjectContainer.js View File

@@ -0,0 +1,64 @@
/*
* 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 { connect } from 'react-redux';
import ComponentNav from './nav/component/ComponentNav';
import { fetchProject } from '../store/rootActions';
import { getComponent } from '../store/rootReducer';

class ProjectContainer extends React.Component {
static propTypes = {
project: React.PropTypes.object,
fetchProject: React.PropTypes.func.isRequired
};

componentDidMount () {
this.props.fetchProject();
}

render () {
if (!this.props.project) {
return null;
}

const isFile = ['FIL', 'UTS'].includes(this.props.project.qualifier);

const configuration = this.props.project.configuration || {};

return (
<div>
{!isFile && (
<ComponentNav component={this.props.project} conf={configuration}/>
)}
{this.props.children}
</div>
);
}
}

const mapStateToProps = (state, ownProps) => ({
project: getComponent(state, ownProps.location.query.id)
});

const mapDispatchToProps = (dispatch, ownProps) => ({
fetchProject: () => dispatch(fetchProject(ownProps.location.query.id))
});

export default connect(mapStateToProps, mapDispatchToProps)(ProjectContainer);

+ 55
- 0
server/sonar-web/src/main/js/app/components/SimpleContainer.js View File

@@ -0,0 +1,55 @@
/*
* 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.
*/
// @flow
import React from 'react';
import GlobalFooter from './GlobalFooter';

export default class SimpleContainer extends React.Component {
static propTypes = {
children: React.PropTypes.element.isRequired
};

componentDidMount () {
document.querySelector('html').classList.add('dashboard-page');
}

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

render () {
return (
<div className="global-container">
<div className="page-wrapper page-wrapper-global" id="container">
<nav className="navbar navbar-global page-container" id="global-navigation">
<div className="navbar-header"></div>
</nav>

<div id="bd" className="page-wrapper-simple">
<div id="nonav" className="page-simple">
{this.props.children}
</div>
</div>
</div>
<GlobalFooter/>
</div>
);
}
}

+ 0
- 91
server/sonar-web/src/main/js/app/components/nav/app.js View File

@@ -1,91 +0,0 @@
/*
* 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 _ from 'underscore';
import React from 'react';
import ReactDOM from 'react-dom';

import GlobalNav from './global/global-nav';
import ComponentNav from './component/component-nav';
import SettingsNav from './settings/settings-nav';
import { getGlobalNavigation, getComponentNavigation, getSettingsNavigation } from '../../../api/nav';

export default class App {
start () {
const options = window.sonarqube;

require('../../../components/workspace/main');

return new Promise(resolve => {
const response = {};
const requests = [];

requests.push(
App.renderGlobalNav(options).then(r => response.global = r)
);

if (options.space === 'component') {
requests.push(
App.renderComponentNav(options).then(r => response.component = r)
);
} else if (options.space === 'settings') {
requests.push(
App.renderSettingsNav(options).then(r => response.settings = r)
);
}

Promise.all(requests).then(() => resolve(response));
});
}

static renderGlobalNav (options) {
return getGlobalNavigation().then(r => {
const el = document.getElementById('global-navigation');
if (el) {
ReactDOM.render(<GlobalNav {...options} {...r}/>, el);
}
return r;
});
}

static renderComponentNav (options) {
return getComponentNavigation(options.componentKey).then(component => {
const el = document.getElementById('context-navigation');
const nextComponent = {
...component,
qualifier: _.last(component.breadcrumbs).qualifier
};
if (el) {
ReactDOM.render(<ComponentNav component={nextComponent} conf={component.configuration || {}}/>, el);
}
return component;
});
}

static renderSettingsNav (options) {
return getSettingsNavigation().then(r => {
const el = document.getElementById('context-navigation');
const opts = _.extend(r, options);
if (el) {
ReactDOM.render(<SettingsNav {...opts}/>, el);
}
return r;
});
}
}

+ 16
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css View File

@@ -0,0 +1,16 @@
.navbar-context {
position: static;
padding: 0;
height: 65px;
}

.navbar-context-inner {
position: fixed;
z-index: 420;
left: 0;
right: 0;
height: 65px;
padding-top: 5px;
box-sizing: border-box;
background-color: #f3f3f3;
}

server/sonar-web/src/main/js/app/components/nav/component/component-nav.js → server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js View File

@@ -23,11 +23,12 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { STATUSES } from '../../../../apps/background-tasks/constants';
import { getTasksForComponent } from '../../../../api/ce';
import ComponentNavFavorite from './component-nav-favorite';
import ComponentNavBreadcrumbs from './component-nav-breadcrumbs';
import ComponentNavMeta from './component-nav-meta';
import ComponentNavMenu from './component-nav-menu';
import ComponentNavFavorite from './ComponentNavFavorite';
import ComponentNavBreadcrumbs from './ComponentNavBreadcrumbs';
import ComponentNavMeta from './ComponentNavMeta';
import ComponentNavMenu from './ComponentNavMenu';
import RecentHistory from './RecentHistory';
import './ComponentNav.css';

export default React.createClass({
componentDidMount() {
@@ -63,25 +64,29 @@ export default React.createClass({

render() {
return (
<div className="container">
<ComponentNavFavorite
component={this.props.component.key}
favorite={this.props.component.isFavorite}
canBeFavorite={this.props.component.canBeFavorite}/>
<nav className="navbar navbar-context page-container" id="context-navigation">
<div className="navbar-context-inner">
<div className="container">
<ComponentNavFavorite
component={this.props.component.key}
favorite={this.props.component.isFavorite}
canBeFavorite={this.props.component.canBeFavorite}/>

<ComponentNavBreadcrumbs
breadcrumbs={this.props.component.breadcrumbs}/>
<ComponentNavBreadcrumbs
breadcrumbs={this.props.component.breadcrumbs}/>

<ComponentNavMeta
{...this.props}
{...this.state}
version={this.props.component.version}
snapshotDate={this.props.component.snapshotDate}/>
<ComponentNavMeta
{...this.props}
{...this.state}
version={this.props.component.version}
snapshotDate={this.props.component.snapshotDate}/>

<ComponentNavMenu
component={this.props.component}
conf={this.props.conf}/>
</div>
<ComponentNavMenu
component={this.props.component}
conf={this.props.conf}/>
</div>
</div>
</nav>
);
}
});

server/sonar-web/src/main/js/app/components/nav/component/component-nav-breadcrumbs.js → server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js View File

@@ -20,11 +20,16 @@
import React from 'react';
import QualifierIcon from '../../../../components/shared/qualifier-icon';

export default React.createClass({
render() {
export default class ComponentNavBreadcrumbs extends React.Component {
static propTypes = {
breadcrumbs: React.PropTypes.array
};

render () {
if (!this.props.breadcrumbs) {
return null;
}

const items = this.props.breadcrumbs.map((item, index) => {
const url = `${window.baseUrl}/dashboard/index?id=${encodeURIComponent(item.key)}`;
return (
@@ -35,8 +40,9 @@ export default React.createClass({
</li>
);
});

return (
<ul className="nav navbar-nav nav-crumbs">{items}</ul>
);
}
});
}

server/sonar-web/src/main/js/app/components/nav/component/component-nav-favorite.js → server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js View File

@@ -20,11 +20,16 @@
import React from 'react';
import Favorite from '../../../../components/controls/Favorite';

export default React.createClass({
render() {
export default class ComponentNavFavorite extends React.Component {
static propTypes = {
canBeFavorite: React.PropTypes.bool.isRequired
};

render () {
if (!this.props.canBeFavorite) {
return null;
}

return (
<div className="navbar-context-favorite">
<Favorite
@@ -33,4 +38,4 @@ export default React.createClass({
</div>
);
}
});
}

server/sonar-web/src/main/js/app/components/nav/component/component-nav-menu.js → server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js View File

@@ -17,10 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import qs from 'querystring';
import classNames from 'classnames';
import React from 'react';
import LinksMixin from '../links-mixin';
import { translate } from '../../../../helpers/l10n';
import { getComponentUrl } from '../../../../helpers/urls';

@@ -37,38 +35,44 @@ const SETTINGS_URLS = [
'/project/deletion'
];

export default React.createClass({
mixins: [LinksMixin],
export default class ComponentNavMenu extends React.Component {
static propTypes = {
component: React.PropTypes.object.isRequired,
conf: React.PropTypes.object.isRequired
};

isDeveloper() {
isDeveloper () {
return this.props.component.qualifier === 'DEV';
},
}

isView() {
isView () {
const { qualifier } = this.props.component;
return qualifier === 'VW' || qualifier === 'SVW';
},

periodParameter() {
const params = qs.parse(window.location.search.substr(1));
return params.period ? `&period=${params.period}` : '';
},

getPeriod() {
const params = qs.parse(window.location.search.substr(1));
return params.period;
},
}

isFixedDashboardActive() {
isFixedDashboardActive () {
const path = window.location.pathname;
return path.indexOf(window.baseUrl + '/dashboard') === 0 || path.indexOf(window.baseUrl + '/governance') === 0;
},
}

shouldShowAdministration() {
shouldShowAdministration () {
return Object.keys(this.props.conf).some(key => this.props.conf[key]);
},
}

renderDashboardLink() {
renderLink (url, title, highlightUrl = url) {
const fullUrl = window.baseUrl + url;
const isActive = typeof highlightUrl === 'string' ?
window.location.pathname.indexOf(window.baseUrl + highlightUrl) === 0 :
highlightUrl(fullUrl);

return (
<li key={url} className={classNames({ 'active': isActive })}>
<a href={fullUrl}>{title}</a>
</li>
);
}

renderDashboardLink () {
const url = getComponentUrl(this.props.component.key);
const name = <i className="icon-home"/>;
const className = classNames({ active: this.isFixedDashboardActive() });
@@ -77,9 +81,9 @@ export default React.createClass({
<a href={url}>{name}</a>
</li>
);
},
}

renderCodeLink() {
renderCodeLink () {
if (this.isDeveloper()) {
return null;
}
@@ -87,19 +91,19 @@ export default React.createClass({
const url = `/code/?id=${encodeURIComponent(this.props.component.key)}`;
const header = this.isView() ? translate('view_projects.page') : translate('code.page');
return this.renderLink(url, header, '/code');
},
}

renderComponentIssuesLink() {
renderComponentIssuesLink () {
const url = `/component_issues?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('issues.page'), '/component_issues');
},
}

renderComponentMeasuresLink() {
renderComponentMeasuresLink () {
const url = `/component_measures/?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('layout.measures'), '/component_measures');
},
}

renderAdministration() {
renderAdministration () {
if (!this.shouldShowAdministration()) {
return null;
}
@@ -126,81 +130,81 @@ export default React.createClass({
</ul>
</li>
);
},
}

renderSettingsLink() {
renderSettingsLink () {
if (!this.props.conf.showSettings) {
return null;
}
const url = `/project/settings?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('project_settings.page'), '/project/settings');
},
}

renderProfilesLink() {
renderProfilesLink () {
if (!this.props.conf.showQualityProfiles) {
return null;
}
const url = `/project/quality_profiles?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('project_quality_profiles.page'), '/project/quality_profiles');
},
}

renderQualityGateLink() {
renderQualityGateLink () {
if (!this.props.conf.showQualityGates) {
return null;
}
const url = `/project/quality_gate?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('project_quality_gate.page'), '/project/quality_gate');
},
}

renderCustomMeasuresLink() {
renderCustomMeasuresLink () {
if (!this.props.conf.showManualMeasures) {
return null;
}
const url = `/custom_measures?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('custom_measures.page'), '/custom_measures');
},
}

renderLinksLink() {
renderLinksLink () {
if (!this.props.conf.showLinks) {
return null;
}
const url = `/project/links?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('project_links.page'), '/project/links');
},
}

renderPermissionsLink() {
renderPermissionsLink () {
if (!this.props.conf.showPermissions) {
return null;
}
const url = `/project_roles?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('permissions.page'), '/project_roles');
},
}

renderHistoryLink() {
renderHistoryLink () {
if (!this.props.conf.showHistory) {
return null;
}
const url = `/project/history?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('project_history.page'), '/project/history');
},
}

renderBackgroundTasksLink() {
renderBackgroundTasksLink () {
if (!this.props.conf.showBackgroundTasks) {
return null;
}
const url = `/project/background_tasks?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('background_tasks.page'), '/project/background_tasks');
},
}

renderUpdateKeyLink() {
renderUpdateKeyLink () {
if (!this.props.conf.showUpdateKey) {
return null;
}
const url = `/project/key?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('update_key.page'), '/project/key');
},
}

renderDeletionLink() {
renderDeletionLink () {
const { qualifier } = this.props.component;

if (qualifier !== 'TRK' && qualifier !== 'VW') {
@@ -209,14 +213,14 @@ export default React.createClass({

const url = `/project/deletion?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, translate('deletion.page'), '/project/deletion');
},
}

renderExtensions() {
renderExtensions () {
const extensions = this.props.conf.extensions || [];
return extensions.map(e => this.renderLink(e.url, e.name, e.url));
},
}

renderTools() {
renderTools () {
const extensions = this.props.component.extensions || [];
const withoutGovernance = extensions.filter(ext => ext.name !== 'Governance');
const tools = withoutGovernance
@@ -237,9 +241,9 @@ export default React.createClass({
</ul>
</li>
);
},
}

render() {
render () {
return (
<ul className="nav navbar-nav nav-tabs">
{this.renderDashboardLink()}
@@ -251,4 +255,4 @@ export default React.createClass({
</ul>
);
}
});
}

server/sonar-web/src/main/js/app/components/nav/component/component-nav-meta.js → server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js View File


server/sonar-web/src/main/js/app/components/nav/__tests__/nav-test.js → server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js View File

@@ -19,13 +19,11 @@
*/
import React from 'react';
import { shallow } from 'enzyme';
import ComponentNavBreadcrumbs from '../component/component-nav-breadcrumbs';
import ComponentNavBreadcrumbs from '../ComponentNavBreadcrumbs';

describe('ComponentNavBreadcrumbs', () => {
it('should not render breadcrumbs with one element', function () {
const breadcrumbs = [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }];
const result = shallow(<ComponentNavBreadcrumbs breadcrumbs={breadcrumbs}/>);
expect(result.find('li').length).toBe(1);
expect(result.find('a').length).toBe(1);
});
it('should not render breadcrumbs with one element', function () {
const breadcrumbs = [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }];
const result = shallow(<ComponentNavBreadcrumbs breadcrumbs={breadcrumbs}/>);
expect(result.find('li').length).toBe(1);
expect(result.find('a').length).toBe(1);
});

server/sonar-web/src/main/js/app/components/nav/global/global-nav.js → server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js View File

@@ -18,22 +18,24 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import GlobalNavBranding from './global-nav-branding';
import GlobalNavMenu from './global-nav-menu';
import GlobalNavUser from './global-nav-user';
import GlobalNavSearch from './global-nav-search';
import ShortcutsHelpView from './shortcuts-help-view';
import { connect } from 'react-redux';
import GlobalNavBranding from './GlobalNavBranding';
import GlobalNavMenu from './GlobalNavMenu';
import GlobalNavUser from './GlobalNavUser';
import GlobalNavSearch from './GlobalNavSearch';
import ShortcutsHelpView from './ShortcutsHelpView';
import { getCurrentUser } from '../../../store/rootReducer';

export default React.createClass({
componentDidMount() {
class GlobalNav extends React.Component {
componentDidMount () {
window.addEventListener('keypress', this.onKeyPress);
},
}

componentWillUnmount() {
componentWillUnmount () {
window.removeEventListener('keypress', this.onKeyPress);
},
}

onKeyPress(e) {
onKeyPress = e => {
const tagName = e.target.tagName;
const code = e.keyCode || e.which;
const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
@@ -42,32 +44,40 @@ export default React.createClass({
if (!isInput && !isModalOpen && isTriggerKey) {
this.openHelp();
}
},
};

openHelp(e) {
openHelp = e => {
if (e) {
e.preventDefault();
}
new ShortcutsHelpView().render();
},
};

render() {
render () {
return (
<div className="container">
<GlobalNavBranding {...this.props}/>
<nav className="navbar navbar-global page-container" id="global-navigation">
<div className="container">
<GlobalNavBranding/>

<GlobalNavMenu {...this.props}/>
<GlobalNavMenu {...this.props}/>

<ul className="nav navbar-nav navbar-right">
<GlobalNavUser {...this.props}/>
<GlobalNavSearch {...this.props}/>
<li>
<a onClick={this.openHelp} href="#">
<i className="icon-help navbar-icon"/>
</a>
</li>
</ul>
</div>
<ul className="nav navbar-nav navbar-right">
<GlobalNavUser {...this.props}/>
<GlobalNavSearch {...this.props}/>
<li>
<a onClick={this.openHelp} href="#">
<i className="icon-help navbar-icon"/>
</a>
</li>
</ul>
</div>
</nav>
);
}
}

const mapStateToProps = state => ({
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(GlobalNav);

server/sonar-web/src/main/js/app/components/nav/global/global-nav-branding.js → server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.js View File

@@ -18,29 +18,42 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { connect } from 'react-redux';
import { getSettingValue, getCurrentUser } from '../../../store/rootReducer';
import { translate } from '../../../../helpers/l10n';

export default React.createClass({
renderLogo() {
const url = this.props.logoUrl || `${window.baseUrl}/images/logo.svg`;
const width = this.props.logoWidth || 100;
class GlobalNavBranding extends React.Component {
static propTypes = {
customLogoUrl: React.PropTypes.string,
customLogoWidth: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number])
};

renderLogo () {
const url = this.props.customLogoUrl || `${window.baseUrl}/images/logo.svg`;
const width = this.props.customLogoWidth || 100;
const height = 30;
const title = translate('layout.sonar.slogan');
return <img src={url}
width={width}
height={height}
alt={title}
title={title}/>;
},
return (
<img src={url} width={width} height={height} alt={title} title={title}/>
);
}

render() {
const homeController = window.SS.user ? '/projects/favorite' : '/about';
render () {
const homeController = this.props.currentUser.isLoggedIn ? '/projects/favorite' : '/about';
const homeUrl = window.baseUrl + homeController;
const homeLinkClassName = 'navbar-brand' + (this.props.logoUrl ? ' navbar-brand-custom' : '');
const homeLinkClassName = 'navbar-brand' + (this.props.customLogoUrl ? ' navbar-brand-custom' : '');
return (
<div className="navbar-header">
<a className={homeLinkClassName} href={homeUrl}>{this.renderLogo()}</a>
</div>
);
}
}

const mapStateToProps = state => ({
currentUser: getCurrentUser(state),
customLogoUrl: (getSettingValue(state, 'sonar.lf.logoUrl') || {}).value,
customLogoWidth: (getSettingValue(state, 'sonar.lf.logoWidthPx') || {}).value
});

export default connect(mapStateToProps)(GlobalNavBranding);

server/sonar-web/src/main/js/app/components/nav/global/global-nav-menu.js → server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js View File

@@ -18,36 +18,42 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import DashboardNameMixin from '../dashboard-name-mixin';
import LinksMixin from '../links-mixin';
import { translate } from '../../../../helpers/l10n';
import { isUserAdmin } from '../../../../helpers/users';

export default React.createClass({
mixins: [DashboardNameMixin, LinksMixin],
export default class GlobalNavMenu extends React.Component {
static propTypes = {
currentUser: React.PropTypes.object.isRequired
};

getDefaultProps () {
return { globalDashboards: [], globalPages: [] };
},
static defaultProps = {
globalDashboards: [],
globalPages: []
};

activeLink (url) {
return window.location.pathname.indexOf(window.baseUrl + url) === 0 ? 'active' : null;
}

renderProjects () {
const controller = window.SS.user ? '/projects/favorite' : '/projects';
const controller = this.props.currentUser.isLoggedIn ? '/projects/favorite' : '/projects';
const url = window.baseUrl + controller;
return (
<li className={this.activeLink('/projects')}>
<a href={url}>{translate('projects.page')}</a>
</li>
);
},
}

renderIssuesLink () {
const query = window.SS.user ? '#resolved=false|assigned_to_me=true' : '#resolved=false';
const query = this.props.currentUser.isLoggedIn ? '#resolved=false|assigned_to_me=true' : '#resolved=false';
const url = window.baseUrl + '/issues' + query;
return (
<li className={this.activeLink('/issues')}>
<a href={url}>{translate('issues.page')}</a>
</li>
);
},
}

renderRulesLink () {
const url = window.baseUrl + '/coding_rules';
@@ -56,16 +62,16 @@ export default React.createClass({
<a href={url}>{translate('coding_rules.page')}</a>
</li>
);
},
}

renderProfilesLink() {
renderProfilesLink () {
const url = window.baseUrl + '/profiles';
return (
<li className={this.activeLink('/profiles')}>
<a href={url}>{translate('quality_profiles.page')}</a>
</li>
);
},
}

renderQualityGatesLink () {
const url = window.baseUrl + '/quality_gates';
@@ -74,10 +80,10 @@ export default React.createClass({
<a href={url}>{translate('quality_gates.page')}</a>
</li>
);
},
}

renderAdministrationLink () {
if (!window.SS.isUserAdmin) {
if (!isUserAdmin(this.props.currentUser)) {
return null;
}
const url = window.baseUrl + '/settings';
@@ -86,7 +92,7 @@ export default React.createClass({
<a className="navbar-admin-link" href={url}>{translate('layout.settings')}</a>
</li>
);
},
}

renderGlobalPageLink (globalPage, index) {
const url = window.baseUrl + globalPage.url;
@@ -95,13 +101,13 @@ export default React.createClass({
<a href={url}>{globalPage.name}</a>
</li>
);
},
}

renderMore () {
if (this.props.globalPages.length === 0) {
return null;
}
const globalPages = this.props.globalPages.map(this.renderGlobalPageLink);
const globalPages = this.props.globalPages.map((p, i) => this.renderGlobalPageLink(p, i));
return (
<li className="dropdown">
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
@@ -113,7 +119,7 @@ export default React.createClass({
</ul>
</li>
);
},
}

render () {
return (
@@ -128,4 +134,4 @@ export default React.createClass({
</ul>
);
}
});
}

server/sonar-web/src/main/js/app/components/nav/global/global-nav-search.js → server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js View File

@@ -19,7 +19,9 @@
*/
import Backbone from 'backbone';
import React from 'react';
import SearchView from './search-view';
import { connect } from 'react-redux';
import SearchView from './SearchView';
import { getCurrentUser } from '../../../store/rootReducer';

function contains (root, node) {
while (node) {
@@ -31,12 +33,10 @@ function contains (root, node) {
return false;
}

export default React.createClass({
getInitialState() {
return { open: false };
},
class GlobalNavSearch extends React.Component {
state = { open: false };

componentDidMount() {
componentDidMount () {
key('s', () => {
const isModalOpen = document.querySelector('html').classList.contains('modal-open');
if (!isModalOpen) {
@@ -44,55 +44,55 @@ export default React.createClass({
}
return false;
});
},
}

componentWillUnmount() {
componentWillUnmount () {
this.closeSearch();
key.unbind('s');
},
}

openSearch() {
openSearch = () => {
document.addEventListener('click', this.onClickOutside);
this.setState({ open: true }, this.renderSearchView);
},
};

closeSearch() {
closeSearch = () => {
document.removeEventListener('click', this.onClickOutside);
this.resetSearchView();
this.setState({ open: false });
},
};

renderSearchView() {
renderSearchView = () => {
const searchContainer = this.refs.container;
this.searchView = new SearchView({
model: new Backbone.Model(this.props),
hide: this.closeSearch
});
this.searchView.render().$el.appendTo(searchContainer);
},
};

resetSearchView() {
resetSearchView = () => {
if (this.searchView) {
this.searchView.destroy();
}
},
};

onClick(e) {
onClick = e => {
e.preventDefault();
if (this.state.open) {
this.closeSearch();
} else {
this.openSearch();
}
},
};

onClickOutside(e) {
onClickOutside = e => {
if (!contains(this.refs.dropdown, e.target)) {
this.closeSearch();
}
},
};

render() {
render () {
const dropdownClassName = 'dropdown' + (this.state.open ? ' open' : '');
return (
<li ref="dropdown" className={dropdownClassName}>
@@ -103,4 +103,10 @@ export default React.createClass({
</li>
);
}
}

const mapStateToProps = state => ({
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(GlobalNavSearch);

server/sonar-web/src/main/js/app/components/nav/global/global-nav-user.js → server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js View File

@@ -22,13 +22,26 @@ import Avatar from '../../../../components/ui/Avatar';
import RecentHistory from '../component/RecentHistory';
import { translate } from '../../../../helpers/l10n';

export default React.createClass({
renderAuthenticated() {
export default class GlobalNavUser extends React.Component {
handleLogin = e => {
e.preventDefault();
const returnTo = window.location.pathname + window.location.search;
window.location = `${window.baseUrl}/sessions/new?return_to=${encodeURIComponent(returnTo)}${window.location.hash}`;
};

handleLogout = e => {
e.preventDefault();
RecentHistory.clear();
window.location = `${window.baseUrl}/sessions/logout`;
};

renderAuthenticated () {
const { currentUser } = this.props;
return (
<li className="dropdown js-user-authenticated">
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
<Avatar email={window.SS.userEmail} size={20}/>&nbsp;
{window.SS.userName}&nbsp;<i className="icon-dropdown"/>
<Avatar email={currentUser.email} size={20}/>&nbsp;
{currentUser.name}&nbsp;<i className="icon-dropdown"/>
</a>
<ul className="dropdown-menu dropdown-menu-right">
<li>
@@ -40,30 +53,17 @@ export default React.createClass({
</ul>
</li>
);
},
}

renderAnonymous() {
renderAnonymous () {
return (
<li>
<a onClick={this.handleLogin} href="#">{translate('layout.login')}</a>
</li>
);
},

handleLogin(e) {
e.preventDefault();
const returnTo = window.location.pathname + window.location.search;
window.location = `${window.baseUrl}/sessions/new?return_to=${encodeURIComponent(returnTo)}${window.location.hash}`;
},

handleLogout(e) {
e.preventDefault();
RecentHistory.clear();
window.location = `${window.baseUrl}/sessions/logout`;
},
}

render() {
const isUserAuthenticated = !!window.SS.user;
return isUserAuthenticated ? this.renderAuthenticated() : this.renderAnonymous();
render () {
return this.props.currentUser.isLoggedIn ? this.renderAuthenticated() : this.renderAnonymous();
}
});
}

server/sonar-web/src/main/js/app/components/nav/global/search-view.js → server/sonar-web/src/main/js/app/components/nav/global/SearchView.js View File

@@ -28,6 +28,7 @@ import SearchTemplate from '../templates/nav-search.hbs';
import RecentHistory from '../component/RecentHistory';
import { translate } from '../../../../helpers/l10n';
import { collapsedDirFromPath, fileFromPath } from '../../../../helpers/path';
import { isUserAdmin } from '../../../../helpers/users';

const SearchItemView = Marionette.ItemView.extend({
tagName: 'li',
@@ -96,7 +97,7 @@ export default Marionette.LayoutView.extend({
const that = this;
this.results = new Backbone.Collection();
this.favorite = [];
if (window.SS.user) {
if (this.model.get('currentUser').isLoggedIn) {
this.fetchFavorite().always(function () {
that.resetResultsToDefault();
});
@@ -226,7 +227,7 @@ export default Marionette.LayoutView.extend({
{ name: translate('quality_gates.page'), url: window.baseUrl + '/quality_gates' }
];
const customItems = [];
if (window.SS.isUserAdmin) {
if (isUserAdmin(this.model.get('currentUser'))) {
customItems.push({ name: translate('layout.settings'), url: window.baseUrl + '/settings' });
}
const findings = [].concat(DEFAULT_ITEMS, customItems).filter(function (f) {

server/sonar-web/src/main/js/app/components/nav/global/shortcuts-help-view.js → server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelpView.js View File


+ 134
- 0
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js View File

@@ -0,0 +1,134 @@
/*
* 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 classNames from 'classnames';
import { translate } from '../../../../helpers/l10n';

export default class SettingsNav extends React.Component {
static defaultProps = {
extensions: []
};

isSomethingActive (urls) {
const path = window.location.pathname;
return urls.some(url => path.indexOf(window.baseUrl + url) === 0);
}

isSecurityActive () {
const urls = ['/users', '/groups', '/roles/global', '/permission_templates'];
return this.isSomethingActive(urls);
}

isProjectsActive () {
const urls = ['/projects_admin', '/background_tasks'];
return this.isSomethingActive(urls);
}

isSystemActive () {
const urls = ['/updatecenter', '/system'];
return this.isSomethingActive(urls);
}

renderLink(url, title, highlightUrl = url) {
const fullUrl = window.baseUrl + url;
const isActive = typeof highlightUrl === 'string' ?
window.location.pathname.indexOf(window.baseUrl + highlightUrl) === 0 :
highlightUrl(fullUrl);

return (
<li key={url} className={classNames({ 'active': isActive })}>
<a href={fullUrl}>{title}</a>
</li>
);
}

render () {
const isSecurity = this.isSecurityActive();
const isProjects = this.isProjectsActive();
const isSystem = this.isSystemActive();

const securityClassName = classNames('dropdown', { active: isSecurity });
const projectsClassName = classNames('dropdown', { active: isProjects });
const systemClassName = classNames('dropdown', { active: isSystem });
const configurationClassNames = classNames('dropdown', {
active: !isSecurity && !isProjects && !isSystem
});

return (
<nav className="navbar navbar-context page-container" id="context-navigation">
<div className="navbar-context-inner">
<div className="container">
<ul className="nav navbar-nav nav-crumbs">
{this.renderLink('/settings', translate('layout.settings'))}
</ul>

<ul className="nav navbar-nav nav-tabs">
<li className={configurationClassNames}>
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
{translate('sidebar.project_settings')} <i className="icon-dropdown"/>
</a>
<ul className="dropdown-menu">
{this.renderLink('/settings', translate('settings.page'), url => window.location.pathname === url)}
{this.renderLink('/settings/licenses', translate('property.category.licenses'))}
{this.renderLink('/settings/encryption', translate('property.category.security.encryption'))}
{this.renderLink('/settings/server_id', translate('property.category.server_id'))}
{this.renderLink('/metrics', 'Custom Metrics')}
{this.props.extensions.map(e => this.renderLink(e.url, e.name))}
</ul>
</li>

<li className={securityClassName}>
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
{translate('sidebar.security')} <i className="icon-dropdown"/>
</a>
<ul className="dropdown-menu">
{this.renderLink('/users', translate('users.page'))}
{this.renderLink('/groups', translate('user_groups.page'))}
{this.renderLink('/roles/global', translate('global_permissions.page'))}
{this.renderLink('/permission_templates', translate('permission_templates'))}
</ul>
</li>

<li className={projectsClassName}>
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
{translate('sidebar.projects')} <i className="icon-dropdown"/>
</a>
<ul className="dropdown-menu">
{this.renderLink('/projects_admin', 'Management')}
{this.renderLink('/background_tasks', translate('background_tasks.page'))}
</ul>
</li>

<li className={systemClassName}>
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
{translate('sidebar.system')} <i className="icon-dropdown"/>
</a>
<ul className="dropdown-menu">
{this.renderLink('/updatecenter', translate('update_center.page'))}
{this.renderLink('/system', translate('system_info.page'))}
</ul>
</li>
</ul>
</div>
</div>
</nav>
);
}
}

+ 0
- 133
server/sonar-web/src/main/js/app/components/nav/settings/settings-nav.js View File

@@ -1,133 +0,0 @@
/*
* 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 classNames from 'classnames';
import some from 'lodash/some';
import LinksMixin from '../links-mixin';
import { translate } from '../../../../helpers/l10n';

export default React.createClass({
mixins: [LinksMixin],

getDefaultProps() {
return { extensions: [] };
},

isSomethingActive(urls) {
const path = window.location.pathname;
return some(urls, url => path.indexOf(window.baseUrl + url) === 0);
},

isSecurityActive() {
const urls = ['/users', '/groups', '/roles/global', '/permission_templates'];
return this.isSomethingActive(urls);
},

isProjectsActive() {
const urls = ['/projects_admin', '/background_tasks'];
return this.isSomethingActive(urls);
},

isSystemActive() {
const urls = ['/updatecenter', '/system'];
return this.isSomethingActive(urls);
},

render() {
const isSecurity = this.isSecurityActive();
const isProjects = this.isProjectsActive();
const isSystem = this.isSystemActive();

const securityClassName = classNames('dropdown', { active: isSecurity });
const projectsClassName = classNames('dropdown', { active: isProjects });
const systemClassName = classNames('dropdown', { active: isSystem });
const configurationClassNames = classNames('dropdown', {
active: !isSecurity && !isProjects && !isSystem
});

return (
<div className="container">
<ul className="nav navbar-nav nav-crumbs">
{this.renderLink('/settings', translate('layout.settings'))}
</ul>

<ul className="nav navbar-nav nav-tabs">
<li className={configurationClassNames}>
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
{translate('sidebar.project_settings')}
{' '}
<i className="icon-dropdown"></i>
</a>
<ul className="dropdown-menu">
{this.renderLink('/settings', translate('settings.page'), url => window.location.pathname === url)}
{this.renderLink('/settings/licenses', translate('property.category.licenses'))}
{this.renderLink('/settings/encryption', translate('property.category.security.encryption'))}
{this.renderLink('/settings/server_id', translate('property.category.server_id'))}
{this.renderLink('/metrics', 'Custom Metrics')}
{this.props.extensions.map(e => this.renderLink(e.url, e.name))}
</ul>
</li>

<li className={securityClassName}>
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
{translate('sidebar.security')}
{' '}
<i className="icon-dropdown"></i>
</a>
<ul className="dropdown-menu">
{this.renderLink('/users', translate('users.page'))}
{this.renderLink('/groups', translate('user_groups.page'))}
{this.renderLink('/roles/global',
translate('global_permissions.page'))}
{this.renderLink('/permission_templates',
translate('permission_templates'))}
</ul>
</li>

<li className={projectsClassName}>
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
{translate('sidebar.projects')}
{' '}
<i className="icon-dropdown"></i>
</a>
<ul className="dropdown-menu">
{this.renderLink('/projects_admin', 'Management')}
{this.renderLink('/background_tasks',
translate('background_tasks.page'))}
</ul>
</li>

<li className={systemClassName}>
<a className="dropdown-toggle" data-toggle="dropdown" href="#">
{translate('sidebar.system')}
{' '}
<i className="icon-dropdown"></i>
</a>
<ul className="dropdown-menu">
{this.renderLink('/updatecenter', translate('update_center.page'))}
{this.renderLink('/system', translate('system_info.page'))}
</ul>
</li>
</ul>
</div>

);
}
});

+ 12
- 2
server/sonar-web/src/main/js/app/index.js View File

@@ -20,12 +20,22 @@
import configureLocale from './utils/configureLocale';
import exposeLibraries from './utils/exposeLibraries';
import startAjaxMonitoring from './utils/startAjaxMonitoring';
import startApp from './utils/startApp';
import startReactApp from './utils/startReactApp';
import { installGlobal } from '../helpers/l10n';
import './styles/index';

require('script!../libs/third-party/jquery-ui.js');
require('script!../libs/third-party/select2.js');
require('script!../libs/third-party/keymaster.js');
require('script!../libs/third-party/bootstrap/tooltip.js');
require('script!../libs/third-party/bootstrap/dropdown.js');
require('script!../libs/select2-jquery-ui-fix.js');
require('script!../libs/inputs.js');
require('script!../libs/jquery-isolated-scroll.js');
require('script!../libs/application.js');

configureLocale();
startAjaxMonitoring();
startApp();
installGlobal();
startReactApp();
exposeLibraries();

+ 75
- 0
server/sonar-web/src/main/js/app/store/appState/duck.js View File

@@ -0,0 +1,75 @@
/*
* 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.
*/
// @flow
type AppState = {
authenticationError: boolean,
authorizationError: boolean,
qualifiers: ?Array<string>
};

export type Action = {
type: string,
appState: AppState
}

export const actions = {
SET_APP_STATE: 'SET_APP_STATE',
REQUIRE_AUTHENTICATION: 'REQUIRE_AUTHENTICATION',
REQUIRE_AUTHORIZATION: 'REQUIRE_AUTHORIZATION'
};

export const setAppState = (appState: AppState): Action => ({
type: actions.SET_APP_STATE,
appState
});

export const requireAuthentication = () => ({
type: actions.REQUIRE_AUTHENTICATION
});

export const requireAuthorization = () => ({
type: actions.REQUIRE_AUTHORIZATION
});

const defaultValue = {
authenticationError: false,
authorizationError: false,
qualifiers: null
};

export default (state: AppState = defaultValue, action: Action) => {
if (action.type === actions.SET_APP_STATE) {
return { ...state, ...action.appState };
}

if (action.type === actions.REQUIRE_AUTHENTICATION) {
return { ...state, authenticationError: true };
}

if (action.type === actions.REQUIRE_AUTHORIZATION) {
return { ...state, authorizationError: true };
}

return state;
};

export const getRootQualifiers = (state: AppState) => (
state.qualifiers
);

+ 48
- 3
server/sonar-web/src/main/js/app/store/rootActions.js View File

@@ -18,13 +18,24 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { getLanguages } from '../../api/languages';
import { getGlobalNavigation, getComponentNavigation } from '../../api/nav';
import * as auth from '../../api/auth';
import { receiveLanguages } from './languages/actions';
import { receiveComponents } from './components/actions';
import { addGlobalErrorMessage } from '../../components/store/globalMessages';
import { parseError } from '../../apps/code/utils';
import { setAppState } from './appState/duck';

const onFail = dispatch => error => {
parseError(error).then(message => dispatch(addGlobalErrorMessage(message)));
};
const onFail = dispatch => error => (
parseError(error).then(message => dispatch(addGlobalErrorMessage(message)))
);

export const fetchAppState = () => dispatch => (
getGlobalNavigation().then(
appState => dispatch(setAppState(appState)),
onFail(dispatch)
)
);

export const fetchLanguages = () => dispatch => {
return getLanguages().then(
@@ -32,3 +43,37 @@ export const fetchLanguages = () => dispatch => {
onFail(dispatch)
);
};

const mapUuidToId = project => ({ ...project, id: project.uuid });

const addQualifier = project => ({
...project,
qualifier: project.breadcrumbs[project.breadcrumbs.length - 1].qualifier
});

export const fetchProject = key => dispatch => (
getComponentNavigation(key).then(
component => dispatch(receiveComponents([mapUuidToId(addQualifier(component))])),
onFail(dispatch)
)
);

export const doLogin = (login, password) => dispatch => (
auth.login(login, password).then(
() => { /* everything is fine */ },
() => {
dispatch(addGlobalErrorMessage('Authentication failed'));
return Promise.reject();
}
)
);

export const doLogout = () => dispatch => (
auth.logout().then(
() => { /* everything is fine */ },
() => {
dispatch(addGlobalErrorMessage('Logout failed'));
return Promise.reject();
}
)
);

+ 10
- 0
server/sonar-web/src/main/js/app/store/rootReducer.js View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { combineReducers } from 'redux';
import appState from './appState/duck';
import components, * as fromComponents from './components/reducer';
import users, * as fromUsers from './users/reducer';
import favorites, * as fromFavorites from './favorites/duck';
@@ -33,6 +34,7 @@ import qualityGatesApp from '../../apps/quality-gates/store/rootReducer';
import settingsApp, * as fromSettingsApp from '../../apps/settings/store/rootReducer';

export default combineReducers({
appState,
components,
globalMessages,
favorites,
@@ -49,6 +51,10 @@ export default combineReducers({
settingsApp
});

export const getAppState = state => (
state.appState
);

export const getComponent = (state, key) => (
fromComponents.getComponent(state.components, key)
);
@@ -125,6 +131,10 @@ export const getPermissionsAppError = state => (
fromPermissionsApp.getError(state.permissionsApp)
);

export const getSettingValue = (state, key) => (
fromSettingsApp.getValue(state.settingsApp, key)
);

export const getSettingsAppDefinition = (state, key) => (
fromSettingsApp.getDefinition(state.settingsApp, key)
);

+ 3
- 3
server/sonar-web/src/main/js/app/store/users/actions.js View File

@@ -26,6 +26,6 @@ export const receiveCurrentUser = user => ({
user
});

export const fetchCurrentUser = () => dispatch => {
getCurrentUser().then(user => dispatch(receiveCurrentUser(user)));
};
export const fetchCurrentUser = () => dispatch => (
getCurrentUser().then(user => dispatch(receiveCurrentUser(user)))
);

+ 2
- 2
server/sonar-web/src/main/js/app/store/users/reducer.js View File

@@ -39,7 +39,7 @@ const userLogins = (state = [], action = {}) => {

const currentUser = (state = null, action = {}) => {
if (action.type === RECEIVE_CURRENT_USER) {
return action.user.isLoggedIn ? action.user.login : false;
return action.user;
}

return state;
@@ -48,5 +48,5 @@ const currentUser = (state = null, action = {}) => {
export default combineReducers({ usersByLogin, userLogins, currentUser });

export const getCurrentUser = state => (
state.currentUser ? state.usersByLogin[state.currentUser] : state.currentUser
state.currentUser
);

server/sonar-web/src/main/js/app/components/nav/dashboard-name-mixin.js → server/sonar-web/src/main/js/app/utils/getCurrentUserFromStore.js View File

@@ -17,16 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { translate } from '../../../helpers/l10n';
// @flow
import { getCurrentUser } from '../store/rootReducer';

export default {
getLocalizedDashboardName(baseName) {
const l10nKey = `dashboard.${baseName}.name`;
const l10nLabel = translate(l10nKey);
if (l10nLabel !== l10nKey) {
return l10nLabel;
} else {
return baseName;
}
}
// TODO remove my usages
const getCurrentUserFromStore = () => {
const getStore = require('./getStore').default;
const store = getStore();
return getCurrentUser(store.getState());
};

export default getCurrentUserFromStore;

+ 35
- 0
server/sonar-web/src/main/js/app/utils/getHistory.js View File

@@ -0,0 +1,35 @@
/*
* 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.
*/
// @flow
import { useRouterHistory } from 'react-router';
import { createHistory } from 'history';

let history;

const ensureHistory = () => {
history = useRouterHistory(createHistory)({
basename: window.baseUrl + '/'
});
return history;
};

export default () => (
history ? history : ensureHistory()
);

+ 33
- 0
server/sonar-web/src/main/js/app/utils/getStore.js View File

@@ -0,0 +1,33 @@
/*
* 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.
*/
// @flow
import configureStore from '../../components/store/configureStore';
import rootReducer from '../store/rootReducer';

let store;

const createStore = () => {
store = configureStore(rootReducer);
return store;
};

export default () => (
store ? store : createStore()
);

+ 36
- 0
server/sonar-web/src/main/js/app/utils/handleRequiredAuthentication.js View File

@@ -0,0 +1,36 @@
/*
* 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.
*/
// @flow
import getStore from './getStore';
import getHistory from './getHistory';
import { requireAuthentication } from '../store/appState/duck';

export default () => {
const store = getStore();
const history = getHistory();

const returnTo = window.location.pathname + window.location.search + window.location.hash;

store.dispatch(requireAuthentication());
history.replace({
pathname: '/sessions/new',
query: { 'return_to': returnTo }
});
};

+ 36
- 0
server/sonar-web/src/main/js/app/utils/handleRequiredAuthorization.js View File

@@ -0,0 +1,36 @@
/*
* 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.
*/
// @flow
import getStore from './getStore';
import getHistory from './getHistory';
import { requireAuthorization } from '../store/appState/duck';

export default () => {
const store = getStore();
const history = getHistory();

const returnTo = window.location.pathname + window.location.search + window.location.hash;

store.dispatch(requireAuthorization());
history.replace({
pathname: '/sessions/new',
query: { 'return_to': returnTo }
});
};

server/sonar-web/src/main/js/app/components/NullComponent.js → server/sonar-web/src/main/js/app/utils/handleRequiredMigration.js View File

@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
export default function () {
return null;
}
// @flow
export default () => {
window.location = window.baseUrl + '/maintenance';
};

+ 0
- 69
server/sonar-web/src/main/js/app/utils/isCurrentPathKnown.js View File

@@ -1,69 +0,0 @@
/*
* 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.
*/
const knownPaths = [
'about',
'account',
'background_tasks',
'coding_rules',
'dashboard',
'groups',
'issues',
'maintenance',
'metrics',
'permission_templates',
'projects',
'projects_admin',
'roles/global',
'settings',
'setup',
'system',
'quality_gates',
'profiles',
'updatecenter',
'users',
'web_api',
'code',
'component_issues',
'component_measures',
'custom_measures',
'project/background_tasks',
'project/settings',
'project/deletion',
'project/quality_profiles',
'project/quality_gate',
'project/links',
'project/key',
'project_roles'
];

const ignoredPaths = [
'users/new'
];

export default function () {
const currentPath = window.location.pathname;

const isIgnored = ignoredPaths.some(path => currentPath.indexOf(`${window.baseUrl}/${path}`) === 0);
if (isIgnored) {
return false;
}

return knownPaths.some(path => currentPath.indexOf(`${window.baseUrl}/${path}`) === 0);
}

+ 7
- 0
server/sonar-web/src/main/js/app/utils/startAjaxMonitoring.js View File

@@ -165,6 +165,12 @@ function handleAjaxError (jqXHR) {
}
}

function handleNotAuthenticatedError () {
// workaround cyclic dependencies
const handleRequiredAuthentication = require('./handleRequiredAuthentication').default;
handleRequiredAuthentication();
}

$.ajaxSetup({
beforeSend (jqXHR) {
jqXHR.setRequestHeader(getCSRFTokenName(), getCSRFTokenValue());
@@ -177,6 +183,7 @@ $.ajaxSetup({
},
statusCode: {
400: handleAjaxError,
401: handleNotAuthenticatedError,
403: handleAjaxError,
500: handleAjaxError,
502: handleAjaxError,

+ 0
- 69
server/sonar-web/src/main/js/app/utils/startApp.js View File

@@ -1,69 +0,0 @@
/*
* 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 _ from 'underscore';
import Navigation from '../components/nav/app';
import { installGlobal, requestMessages } from '../../helpers/l10n';

function requestLocalizationBundle () {
if (!window.sonarqube.bannedNavigation) {
installGlobal();
return requestMessages();
} else {
return Promise.resolve();
}
}

function startNavigation () {
if (!window.sonarqube.bannedNavigation) {
return new Navigation().start();
} else {
return Promise.resolve();
}
}

function prepareAppOptions (navResponse) {
const appOptions = { el: '#content' };
if (navResponse) {
appOptions.rootQualifiers = navResponse.global.qualifiers;
appOptions.logoUrl = navResponse.global.logoUrl;
appOptions.logoWidth = navResponse.global.logoWidth;
if (navResponse.component) {
appOptions.component = {
id: navResponse.component.uuid,
key: navResponse.component.key,
name: navResponse.component.name,
qualifier: _.last(navResponse.component.breadcrumbs).qualifier,
breadcrumbs: navResponse.component.breadcrumbs,
snapshotDate: navResponse.component.snapshotDate
};
}
}
return appOptions;
}

const startApp = () => {
window.sonarqube.appStarted = Promise.resolve()
.then(requestLocalizationBundle)
.then(startNavigation)
.then(prepareAppOptions);
};

export default startApp;


+ 72
- 53
server/sonar-web/src/main/js/app/utils/startReactApp.js View File

@@ -19,17 +19,23 @@
*/
import React from 'react';
import { render } from 'react-dom';
import { Router, Route, useRouterHistory } from 'react-router';
import { createHistory } from 'history';
import { Router, Route, IndexRoute } from 'react-router';
import { Provider } from 'react-redux';
import LocalizationContainer from '../components/LocalizationContainer';
import MigrationContainer from '../components/MigrationContainer';
import App from '../components/App';
import ComponentContainer from '../components/ComponentContainer';
import NullComponent from '../components/NullComponent';
import GlobalContainer from '../components/GlobalContainer';
import SimpleContainer from '../components/SimpleContainer';
import Landing from '../components/Landing';
import ProjectContainer from '../components/ProjectContainer';
import AdminContainer from '../components/AdminContainer';
import NotFound from '../components/NotFound';
import aboutRoutes from '../../apps/about/routes';
import accountRoutes from '../../apps/account/routes';
import backgroundTasksRoutes from '../../apps/background-tasks/routes';
import codeRoutes from '../../apps/code/routes';
import codingRulesRoutes from '../../apps/coding-rules/routes';
import componentRoutes from '../../apps/component/routes';
import componentIssuesRoutes from '../../apps/component-issues/routes';
import componentMeasuresRoutes from '../../apps/component-measures/routes';
import customMeasuresRoutes from '../../apps/custom-measures/routes';
@@ -43,6 +49,7 @@ import projectsRoutes from '../../apps/projects/routes';
import projectsAdminRoutes from '../../apps/projects-admin/routes';
import qualityGatesRoutes from '../../apps/quality-gates/routes';
import qualityProfilesRoutes from '../../apps/quality-profiles/routes';
import sessionsRoutes from '../../apps/sessions/routes';
import settingsRoutes from '../../apps/settings/routes';
import systemRoutes from '../../apps/system/routes';
import updateCenterRoutes from '../../apps/update-center/routes';
@@ -50,67 +57,79 @@ import usersRoutes from '../../apps/users/routes';
import webAPIRoutes from '../../apps/web-api/routes';
import { maintenanceRoutes, setupRoutes } from '../../apps/maintenance/routes';
import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/permissions/routes';
import configureStore from '../../components/store/configureStore';
import rootReducer from '../store/rootReducer';
import isCurrentPathKnown from './isCurrentPathKnown';
import getStore from './getStore';
import getHistory from './getHistory';

const startReactApp = () => {
if (isCurrentPathKnown()) {
window.sonarqube.appStarted.then(options => {
const el = document.querySelector(options.el);
const el = document.getElementById('content');

const history = useRouterHistory(createHistory)({
basename: window.baseUrl + '/'
});
const history = getHistory();
const store = getStore();

const store = configureStore(rootReducer);
render((
<Provider store={store}>
<Router history={history}>
<Route component={LocalizationContainer}>
<Route component={SimpleContainer}>
<Route path="maintenance">{maintenanceRoutes}</Route>
<Route path="setup">{setupRoutes}</Route>
</Route>

<Route component={MigrationContainer}>
<Route component={SimpleContainer}>
<Route path="/sessions">{sessionsRoutes}</Route>
</Route>

render((
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<Route path="about">{aboutRoutes}</Route>
<Route path="account">{accountRoutes}</Route>
<Route path="background_tasks">{backgroundTasksRoutes}</Route>
<Route path="coding_rules">{codingRulesRoutes}</Route>
<Route path="dashboard">{overviewRoutes}</Route>
<Route path="groups">{groupsRoutes}</Route>
<Route path="issues">{issuesRoutes}</Route>
<Route path="maintenance">{maintenanceRoutes}</Route>
<Route path="metrics">{metricsRoutes}</Route>
<Route path="permission_templates">{permissionTemplatesRoutes}</Route>
<Route path="projects">{projectsRoutes}</Route>
<Route path="projects_admin">{projectsAdminRoutes}</Route>
<Route path="roles/global">{globalPermissionsRoutes}</Route>
<Route path="settings">{settingsRoutes}</Route>
<Route path="setup">{setupRoutes}</Route>
<Route path="system">{systemRoutes}</Route>
<Route path="quality_gates">{qualityGatesRoutes}</Route>
<Route path="profiles">{qualityProfilesRoutes}</Route>
<Route path="updatecenter">{updateCenterRoutes}</Route>
<Route path="users">{usersRoutes}</Route>
<Route path="web_api">{webAPIRoutes}</Route>

<Route component={ComponentContainer}>
<Route path="code">{codeRoutes}</Route>
<Route path="component_issues">{componentIssuesRoutes}</Route>
<Route path="component_measures">{componentMeasuresRoutes}</Route>
<Route path="custom_measures">{customMeasuresRoutes}</Route>
<Route path="project">
<IndexRoute component={Landing}/>

<Route component={GlobalContainer}>
<Route path="about">{aboutRoutes}</Route>
<Route path="account">{accountRoutes}</Route>
<Route path="coding_rules">{codingRulesRoutes}</Route>
<Route path="component">{componentRoutes}</Route>
<Route path="issues">{issuesRoutes}</Route>
<Route path="projects">{projectsRoutes}</Route>
<Route path="quality_gates">{qualityGatesRoutes}</Route>
<Route path="profiles">{qualityProfilesRoutes}</Route>
<Route path="web_api">{webAPIRoutes}</Route>

<Route component={ProjectContainer}>
<Route path="code">{codeRoutes}</Route>
<Route path="component_issues">{componentIssuesRoutes}</Route>
<Route path="component_measures">{componentMeasuresRoutes}</Route>
<Route path="custom_measures">{customMeasuresRoutes}</Route>
<Route path="dashboard">{overviewRoutes}</Route>
<Route path="project">
<Route path="background_tasks">{backgroundTasksRoutes}</Route>
<Route path="settings">{settingsRoutes}</Route>
{projectAdminRoutes}
</Route>
<Route path="project_roles">{projectPermissionsRoutes}</Route>
</Route>

<Route component={AdminContainer}>
<Route path="background_tasks">{backgroundTasksRoutes}</Route>
<Route path="groups">{groupsRoutes}</Route>
<Route path="metrics">{metricsRoutes}</Route>
<Route path="permission_templates">{permissionTemplatesRoutes}</Route>
<Route path="projects_admin">{projectsAdminRoutes}</Route>
<Route path="roles/global">{globalPermissionsRoutes}</Route>
<Route path="settings">{settingsRoutes}</Route>
{projectAdminRoutes}
<Route path="system">{systemRoutes}</Route>
<Route path="updatecenter">{updateCenterRoutes}</Route>
<Route path="users">{usersRoutes}</Route>
</Route>
<Route path="project_roles">{projectPermissionsRoutes}</Route>
</Route>
</Route>

<Route path="*" component={NullComponent}/>
</Router>
</Provider>
), el);
});
}
<Route path="*" component={NotFound}/>
</Route>
</Route>
</Route>
</Router>
</Provider>
), el);
};

export default startReactApp;

+ 31
- 13
server/sonar-web/src/main/js/apps/about/components/AboutApp.js View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { connect } from 'react-redux';
import keyBy from 'lodash/keyBy';
import AboutProjects from './AboutProjects';
import EntryIssueTypes from './EntryIssueTypes';
@@ -28,11 +29,18 @@ import AboutLeakPeriod from './AboutLeakPeriod';
import AboutStandards from './AboutStandards';
import AboutScanners from './AboutScanners';
import { translate } from '../../../helpers/l10n';
import '../styles.css';
import { searchProjects } from '../../../api/components';
import { getFacet } from '../../../api/issues';
import { getSettingValue } from '../../../app/store/rootReducer';
import * as settingsAPI from '../../../api/settings';
import '../styles.css';

class AboutApp extends React.Component {
static propTypes = {
customLogoUrl: React.PropTypes.string,
customLogoWidth: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number])
};

export default class AboutApp extends React.Component {
state = {
loading: true
};
@@ -56,20 +64,23 @@ export default class AboutApp extends React.Component {
return getFacet({ resolved: false }, 'types');
}

loadCustomText () {
return settingsAPI.getSettingValue('sonar.lf.aboutText');
}

loadData () {
Promise.all([
window.sonarqube.appStarted,
this.loadProjects(),
this.loadIssues()
this.loadIssues(),
this.loadCustomText()
]).then(responses => {
if (this.mounted) {
const [options, projectsCount, issues] = responses;
const [projectsCount, issues, customText] = responses;
const issueTypes = keyBy(issues.facet, 'val');
this.setState({
projectsCount,
issueTypes,
logoUrl: options.logoUrl,
logoWidth: options.logoWidth,
customText,
loading: false
});
}
@@ -81,12 +92,12 @@ export default class AboutApp extends React.Component {
return null;
}

const { landingText } = window.sonarqube;
const { customText } = this.state;

const logoUrl = this.state.logoUrl || `${window.baseUrl}/images/logo.svg`;
const logoWidth = this.state.logoWidth || 100;
const logoUrl = this.props.customLogoUrl || `${window.baseUrl}/images/logo.svg`;
const logoWidth = Number(this.props.customLogoWidth || 100);
const logoHeight = 30;
const logoTitle = this.state.logoUrl ? '' : translate('layout.sonar.slogan');
const logoTitle = this.props.customLogoUrl ? '' : translate('layout.sonar.slogan');

return (
<div id="about-page" className="about-page">
@@ -113,8 +124,8 @@ export default class AboutApp extends React.Component {

<div className="about-page-container">

{landingText.length > 0 && (
<div className="about-page-section" dangerouslySetInnerHTML={{ __html: landingText }}/>
{customText != null && customText.length > 0 && (
<div className="about-page-section" dangerouslySetInnerHTML={{ __html: customText }}/>
)}

<div className="columns">
@@ -142,3 +153,10 @@ export default class AboutApp extends React.Component {
);
}
}

const mapStateToProps = state => ({
customLogoUrl: (getSettingValue(state, 'sonar.lf.logoUrl') || {}).value,
customLogoWidth: (getSettingValue(state, 'sonar.lf.logoWidthPx') || {}).value
});

export default connect(mapStateToProps)(AboutApp);

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

@@ -28,16 +28,6 @@ class Account extends React.Component {
render () {
const { currentUser, children } = this.props;

if (currentUser == null) {
return (
<div id="account-page">
<div className="text-center">
<i className="spinner"/>
</div>
</div>
);
}

return (
<div id="account-page">
<header className="account-header">

+ 10
- 3
server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js View File

@@ -17,11 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* @flow */
// @flow
import React from 'react';
import shallowCompare from 'react-addons-shallow-compare';
import debounce from 'lodash/debounce';
import { connect } from 'react-redux';
import { DEFAULT_FILTERS, DEBOUNCE_DELAY, STATUSES, CURRENTS } from './../constants';
import Header from './Header';
import Footer from './Footer';
@@ -31,9 +31,10 @@ import Tasks from '../components/Tasks';
import { getTypes, getActivity, getStatus, cancelAllTasks, cancelTask as cancelTaskAPI } from '../../../api/ce';
import { updateTask, mapFiltersToParameters } from '../utils';
import { Task } from '../types';
import { getComponent } from '../../../app/store/rootReducer';
import '../background-tasks.css';

export default class BackgroundTasksApp extends React.Component {
class BackgroundTasksApp extends React.Component {
static contextTypes = {
router: React.PropTypes.object.isRequired
};
@@ -229,3 +230,9 @@ export default class BackgroundTasksApp extends React.Component {
);
}
}

const mapStateToProps = (state, ownProps) => ({
component: ownProps.location.query.id ? getComponent(state, ownProps.location.query.id) : undefined
});

export default connect(mapStateToProps)(BackgroundTasksApp);

+ 9
- 3
server/sonar-web/src/main/js/apps/code/components/App.js View File

@@ -19,7 +19,7 @@
*/
import classNames from 'classnames';
import React from 'react';
import { connect } from 'react-redux';
import Components from './Components';
import Breadcrumbs from './Breadcrumbs';
import SourceViewer from './../../../components/source-viewer/SourceViewer';
@@ -27,10 +27,10 @@ import Search from './Search';
import ListFooter from '../../../components/controls/ListFooter';
import { retrieveComponentChildren, retrieveComponent, loadMoreChildren, parseError } from '../utils';
import { addComponent, addComponentBreadcrumbs } from '../bucket';
import { getComponent } from '../../../app/store/rootReducer';
import '../code.css';

export default class App extends React.Component {
class App extends React.Component {
state = {
loading: true,
baseComponent: null,
@@ -210,3 +210,9 @@ export default class App extends React.Component {
);
}
}

const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id)
});

export default connect(mapStateToProps)(App);

+ 11
- 2
server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js View File

@@ -19,13 +19,22 @@
*/
import React from 'react';
import init from '../init';
import { connect } from 'react-redux';
import { getComponent, getCurrentUser } from '../../../app/store/rootReducer';

export default class ComponentIssuesAppContainer extends React.Component {
class ComponentIssuesAppContainer extends React.Component {
componentDidMount () {
init(this.refs.container);
init(this.refs.container, this.props.component, this.props.currentUser);
}

render () {
return <div ref="container"/>;
}
}

const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id),
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(ComponentIssuesAppContainer);

+ 15
- 13
server/sonar-web/src/main/js/apps/component-issues/init.js View File

@@ -33,17 +33,19 @@ import FacetsView from './../issues/facets-view';
import HeaderView from './../issues/HeaderView';

const App = new Marionette.Application();
const init = function (el) {
const options = window.sonarqube;

this.config = options.config;
const init = function ({ el, component, currentUser }) {
this.config = {
resource: component.id,
resourceName: component.name,
resourceQualifier: component.qualifier
};
this.state = new State({
canBulkChange: !!window.SS.user,
canBulkChange: currentUser.isLoggedIn,
isContext: true,
contextQuery: { componentUuids: options.config.resource },
contextComponentUuid: options.config.resource,
contextComponentName: options.config.resourceName,
contextComponentQualifier: options.config.resourceQualifier
contextQuery: { componentUuids: this.config.resource },
contextComponentUuid: this.config.resource,
contextComponentName: this.config.resourceName,
contextComponentQualifier: this.config.resourceQualifier
});
this.updateContextFacets();
this.list = new Issues();
@@ -109,10 +111,10 @@ App.updateContextFacets = function () {
});
};

App.on('start', function (el) {
init.call(App, el);
App.on('start', function (options) {
init.call(App, options);
});

export default function (el) {
App.start(el);
export default function (el, component, currentUser) {
App.start({ el, component, currentUser });
}

+ 5
- 6
server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js View File

@@ -20,13 +20,12 @@
import { connect } from 'react-redux';
import App from './App';
import { fetchMetrics, setComponent } from './actions';
import { getMeasuresAppAllMetrics } from '../../../app/store/rootReducer';
import { getComponent, getMeasuresAppAllMetrics } from '../../../app/store/rootReducer';

const mapStateToProps = state => {
return {
metrics: getMeasuresAppAllMetrics(state)
};
};
const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id),
metrics: getMeasuresAppAllMetrics(state)
});

const mapDispatchToProps = dispatch => {
return {

server/sonar-web/src/main/js/app/components/ComponentContainer.js → server/sonar-web/src/main/js/apps/component/components/App.js View File

@@ -17,15 +17,18 @@
* 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 SourceViewer from '../../../components/source-viewer/SourceViewer';
import { getComponentNavigation } from '../../../api/nav';

export default class ComponentContainer extends React.Component {
export default class App extends React.Component {
state = {};

componentDidMount () {
window.sonarqube.appStarted.then(options => {
this.setState({ component: options.component });
});
getComponentNavigation(this.props.location.query.id).then(component => (
this.setState({ component })
));
}

render () {
@@ -33,8 +36,10 @@ export default class ComponentContainer extends React.Component {
return null;
}

return React.cloneElement(this.props.children, {
component: this.state.component
});
return (
<div className="page">
<SourceViewer component={{ id: this.state.component.uuid }}/>
</div>
);
}
}

+ 28
- 0
server/sonar-web/src/main/js/apps/component/routes.js View File

@@ -0,0 +1,28 @@
/*
* 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.
*/
// @flow
import React from 'react';
import { IndexRoute, Redirect } from 'react-router';
import App from './components/App';

export default [
<Redirect key="1" from="/component/index" to="/component"/>,
<IndexRoute key="2" component={App}/>
];

+ 10
- 10
server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js View File

@@ -18,22 +18,22 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { connect } from 'react-redux';
import init from '../init';
import { getComponent } from '../../../app/store/rootReducer';

export default class CustomMeasuresAppContainer extends React.Component {
class CustomMeasuresAppContainer extends React.Component {
componentDidMount () {
if (this.props.component) {
init(this.refs.container, this.props.component);
}
}

componentDidUpdate () {
if (this.props.component) {
init(this.refs.container, this.props.component);
}
init(this.refs.container, this.props.component);
}

render () {
return <div ref="container"/>;
}
}

const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id)
});

export default connect(mapStateToProps)(CustomMeasuresAppContainer);

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/HeaderView.js View File

@@ -54,7 +54,7 @@ export default Marionette.ItemView.extend({
...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
me,
isContext: this.options.app.state.get('isContext'),
user: window.SS.user
user: this.options.app.state.get('user')
};
}
});

+ 14
- 2
server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js View File

@@ -18,14 +18,26 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { connect } from 'react-redux';
import init from '../init';
import { getCurrentUser } from '../../../app/store/rootReducer';

class IssuesAppContainer extends React.Component {
static propTypes = {
currentUser: React.PropTypes.any.isRequired
};

export default class IssuesAppContainer extends React.Component {
componentDidMount () {
init(this.refs.container);
init(this.refs.container, this.props.currentUser);
}

render () {
return <div ref="container"/>;
}
}

const mapStateToProps = state => ({
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(IssuesAppContainer);

+ 4
- 4
server/sonar-web/src/main/js/apps/issues/init.js View File

@@ -32,8 +32,8 @@ import FacetsView from './facets-view';
import HeaderView from './HeaderView';

const App = new Marionette.Application();
const init = function (el) {
this.state = new State({ canBulkChange: !!window.SS.user });
const init = function ({ el, user }) {
this.state = new State({ user, canBulkChange: user.isLoggedIn });
this.list = new Issues();
this.facets = new Facets();

@@ -76,7 +76,7 @@ App.on('start', function (el) {
init.call(App, el);
});

export default function (el) {
App.start(el);
export default function (el, user) {
App.start({ el, user });
}


+ 9
- 1
server/sonar-web/src/main/js/apps/overview/components/App.js View File

@@ -19,7 +19,6 @@
*/
import React from 'react';
import shallowCompare from 'react-addons-shallow-compare';

import OverviewApp from './OverviewApp';
import EmptyOverview from './EmptyOverview';
import { ComponentType } from '../propTypes';
@@ -36,6 +35,15 @@ export default class App extends React.Component {
render () {
const { component } = this.props;

if (['FIL', 'UTS'].includes(component.qualifier)) {
const SourceViewer = require('../../../components/source-viewer/SourceViewer').default;
return (
<div className="page">
<SourceViewer component={component}/>
</div>
);
}

if (!component.snapshotDate) {
return <EmptyOverview {...this.props}/>;
}

+ 6
- 26
server/sonar-web/src/main/js/apps/overview/components/AppContainer.js View File

@@ -17,32 +17,12 @@
* 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 { connect } from 'react-redux';
import App from './App';
import { getComponent } from '../../../app/store/rootReducer';

export default class AppContainer extends React.Component {
state = {};
const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id)
});

componentDidMount () {
window.sonarqube.appStarted.then(options => {
this.setState({ component: options.component });
});
}

render () {
// workaround for the case when a file is displayed
if (window.sonarqube.file) {
return null;
}

if (!this.state.component) {
return null;
}

const component = { ...this.state.component, ...window.sonarqube.overview.component };

return (
<App component={component}/>
);
}
}
export default connect(mapStateToProps)(App);

+ 5
- 5
server/sonar-web/src/main/js/apps/overview/meta/Meta.js View File

@@ -27,15 +27,15 @@ import EventsList from './../events/EventsList';
import MetaSize from './MetaSize';

const Meta = ({ component, measures }) => {
const { qualifier, description, profiles, gate } = component;
const { qualifier, description, qualityProfiles, qualityGate } = component;

const isProject = qualifier === 'TRK';
const isView = qualifier === 'VW' || qualifier === 'SVW';
const isDeveloper = qualifier === 'DEV';

const hasDescription = !!description;
const hasQualityProfiles = Array.isArray(profiles) && profiles.length > 0;
const hasQualityGate = !!gate;
const hasQualityProfiles = Array.isArray(qualityProfiles) && qualityProfiles.length > 0;
const hasQualityGate = !!qualityGate;

const shouldShowQualityProfiles = !isView && !isDeveloper && hasQualityProfiles;
const shouldShowQualityGate = !isView && !isDeveloper && hasQualityGate;
@@ -53,11 +53,11 @@ const Meta = ({ component, measures }) => {
<MetaSize component={component} measures={measures}/>

{shouldShowQualityGate && (
<MetaQualityGate gate={gate}/>
<MetaQualityGate gate={qualityGate}/>
)}

{shouldShowQualityProfiles && (
<MetaQualityProfiles profiles={profiles}/>
<MetaQualityProfiles profiles={qualityProfiles}/>
)}

<MetaLinks component={component}/>

+ 13
- 2
server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js View File

@@ -18,12 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { connect } from 'react-redux';
import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getQualityProfileUrl } from '../../../helpers/urls';
import { searchRules } from '../../../api/rules';
import { getLanguages } from '../../../app/store/rootReducer';

export default class MetaQualityProfiles extends React.Component {
class MetaQualityProfiles extends React.Component {
state = {
deprecatedByKey: {}
};
@@ -69,10 +71,13 @@ export default class MetaQualityProfiles extends React.Component {
}

renderProfile (profile) {
const languageFromStore = this.props.languages[profile.language];
const languageName = languageFromStore ? languageFromStore.name : profile.language;

const inner = (
<div className="text-ellipsis">
<span className="note spacer-right">
{'(' + profile.language + ')'}
{'(' + languageName + ')'}
</span>
<a href={getQualityProfileUrl(profile.key)}>
{profile.name}
@@ -120,3 +125,9 @@ export default class MetaQualityProfiles extends React.Component {
);
}
}

const mapStateToProps = state => ({
languages: getLanguages(state)
});

export default connect(mapStateToProps)(MetaQualityProfiles);

+ 9
- 19
server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js View File

@@ -17,25 +17,15 @@
* 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 { connect } from 'react-redux';
import App from './App';
import { getAppState } from '../../../app/store/rootReducer';
import { getRootQualifiers } from '../../../app/store/appState/duck';

export default class AppContainer extends React.Component {
state = {};
const mapStateToProps = state => ({
topQualifiers: getRootQualifiers(getAppState(state)),
});

componentDidMount () {
window.sonarqube.appStarted.then(options => {
this.setState({ rootQualifiers: options.rootQualifiers });
});
}

render () {
if (!this.state.rootQualifiers) {
return null;
}

return (
<App {...this.props} topQualifiers={this.state.rootQualifiers}/>
);
}
}
export default connect(
mapStateToProps
)(App);

+ 12
- 2
server/sonar-web/src/main/js/apps/permissions/project/components/App.js View File

@@ -18,14 +18,16 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { connect } from 'react-redux';
import PageHeader from './PageHeader';
import AllHoldersList from './AllHoldersList';
import PageError from '../../shared/components/PageError';
import { getComponent, getCurrentUser } from '../../../../app/store/rootReducer';
import '../../styles.css';

// TODO helmet

export default class App extends React.Component {
class App extends React.Component {
static propTypes = {
component: React.PropTypes.object
};
@@ -37,10 +39,18 @@ export default class App extends React.Component {

return (
<div className="page page-limited">
<PageHeader project={this.props.component}/>
<PageHeader project={this.props.component} currentUser={this.props.currentUser}/>
<PageError/>
<AllHoldersList project={this.props.component}/>
</div>
);
}
}

const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id),
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(App);


+ 2
- 1
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js View File

@@ -23,6 +23,7 @@ import { translate } from '../../../../helpers/l10n';
import ApplyTemplateView from '../views/ApplyTemplateView';
import { loadHolders } from '../store/actions';
import { isPermissionsAppLoading } from '../../../../app/store/rootReducer';
import { isUserAdmin } from '../../../../helpers/users';

class PageHeader extends React.Component {
static propTypes = {
@@ -59,7 +60,7 @@ class PageHeader extends React.Component {
<i className="spinner"/>
)}

{!!window.SS.isUserAdmin && (
{isUserAdmin(this.props.currentUser) && (
<div className="page-actions">
<button className="js-apply-template" onClick={this.handleApplyTemplate}>
Apply Template

+ 9
- 1
server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js View File

@@ -18,10 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { connect } from 'react-redux';
import Header from './Header';
import Form from './Form';
import { getComponent } from '../../../app/store/rootReducer';

export default class Deletion extends React.Component {
class Deletion extends React.Component {
static propTypes = {
component: React.PropTypes.object
};
@@ -39,3 +41,9 @@ export default class Deletion extends React.Component {
);
}
}

const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id)
});

export default connect(mapStateToProps)(Deletion);

+ 3
- 2
server/sonar-web/src/main/js/apps/project-admin/key/Key.js View File

@@ -35,7 +35,7 @@ import {
import { parseError } from '../../code/utils';
import { reloadUpdateKeyPage } from './utils';
import RecentHistory from '../../../app/components/nav/component/RecentHistory';
import { getProjectAdminProjectModules } from '../../../app/store/rootReducer';
import { getProjectAdminProjectModules, getComponent } from '../../../app/store/rootReducer';

class Key extends React.Component {
static propTypes = {
@@ -152,7 +152,8 @@ class Key extends React.Component {
}

const mapStateToProps = (state, ownProps) => ({
modules: getProjectAdminProjectModules(state, ownProps.component.key)
component: getComponent(state, ownProps.location.query.id),
modules: getProjectAdminProjectModules(state, ownProps.location.query.id)
});

export default connect(

+ 3
- 2
server/sonar-web/src/main/js/apps/project-admin/links/Links.js View File

@@ -28,7 +28,7 @@ import {
deleteProjectLink,
createProjectLink
} from '../store/actions';
import { getProjectAdminProjectLinks } from '../../../app/store/rootReducer';
import { getProjectAdminProjectLinks, getComponent } from '../../../app/store/rootReducer';

class Links extends React.Component {
static propTypes = {
@@ -73,7 +73,8 @@ class Links extends React.Component {
}

const mapStateToProps = (state, ownProps) => ({
links: getProjectAdminProjectLinks(state, ownProps.component.key)
component: getComponent(state, ownProps.location.query.id),
links: getProjectAdminProjectLinks(state, ownProps.location.query.id)
});

export default connect(

+ 3
- 2
server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js View File

@@ -24,7 +24,7 @@ import Header from './Header';
import Form from './Form';
import GlobalMessagesContainer from '../components/GlobalMessagesContainer';
import { fetchProjectGate, setProjectGate } from '../store/actions';
import { getProjectAdminAllGates, getProjectAdminProjectGate } from '../../../app/store/rootReducer';
import { getProjectAdminAllGates, getProjectAdminProjectGate, getComponent } from '../../../app/store/rootReducer';

class QualityGate extends React.Component {
static propTypes = {
@@ -60,8 +60,9 @@ class QualityGate extends React.Component {
}

const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id),
allGates: getProjectAdminAllGates(state),
gate: getProjectAdminProjectGate(state, ownProps.component.key)
gate: getProjectAdminProjectGate(state, ownProps.location.query.id)
});

export default connect(

+ 7
- 2
server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js View File

@@ -24,7 +24,11 @@ import Header from './Header';
import Table from './Table';
import GlobalMessagesContainer from '../components/GlobalMessagesContainer';
import { fetchProjectProfiles, setProjectProfile } from '../store/actions';
import { getProjectAdminAllProfiles, getProjectAdminProjectProfiles } from '../../../app/store/rootReducer';
import {
getProjectAdminAllProfiles,
getProjectAdminProjectProfiles,
getComponent
} from '../../../app/store/rootReducer';

class QualityProfiles extends React.Component {
static propTypes = {
@@ -68,8 +72,9 @@ class QualityProfiles extends React.Component {
}

const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id),
allProfiles: getProjectAdminAllProfiles(state),
profiles: getProjectAdminProjectProfiles(state, ownProps.component.key)
profiles: getProjectAdminProjectProfiles(state, ownProps.location.query.id)
});

export default connect(

+ 5
- 15
server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js View File

@@ -17,36 +17,26 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { connect } from 'react-redux';
import React from 'react';
import { connect } from 'react-redux';
import Main from './main';
import { getCurrentUser } from '../../app/store/rootReducer';
import { getCurrentUser, getAppState } from '../../app/store/rootReducer';
import { getRootQualifiers } from '../../app/store/appState/duck';

class AppContainer extends React.Component {
state = {};

componentDidMount () {
window.sonarqube.appStarted.then(options => {
this.setState({ rootQualifiers: options.rootQualifiers });
});
}

render () {
if (!this.props.user || !this.state.rootQualifiers) {
return null;
}

const hasProvisionPermission = this.props.user.permissions.global.indexOf('provisioning') !== -1;

return (
<Main
hasProvisionPermission={hasProvisionPermission}
topLevelQualifiers={this.state.rootQualifiers}/>
topLevelQualifiers={this.props.rootQualifiers}/>
);
}
}

const mapStateToProps = state => ({
rootQualifiers: getRootQualifiers(getAppState(state)),
user: getCurrentUser(state)
});


+ 0
- 4
server/sonar-web/src/main/js/apps/projects/components/AllProjects.js View File

@@ -51,10 +51,6 @@ export default class AllProjects extends React.Component {
}

render () {
if (this.props.user == null) {
return null;
}

const isFiltered = Object.keys(this.state.query).some(key => this.state.query[key] != null);

return (

+ 1
- 1
server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.js View File

@@ -23,7 +23,7 @@ import { translate } from '../../../helpers/l10n';

export default class FavoriteFilter extends React.Component {
render () {
if (!this.props.user) {
if (!this.props.user.isLoggedIn) {
return null;
}


+ 6
- 8
server/sonar-web/src/main/js/apps/quality-profiles/components/App.js View File

@@ -19,9 +19,8 @@
*/
import React from 'react';
import { getQualityProfiles, getExporters } from '../../../api/quality-profiles';
import { getCurrentUser } from '../../../api/users';
import '../styles.css';
import { sortProfiles } from '../utils';
import '../styles.css';

export default class App extends React.Component {
state = { loading: true };
@@ -44,16 +43,13 @@ export default class App extends React.Component {
loadData () {
this.setState({ loading: true });
Promise.all([
getCurrentUser(),
getExporters(),
getQualityProfiles()
]).then(responses => {
if (this.mounted) {
const [user, exporters, profiles] = responses;
const canAdmin = user.permissions.global.includes('profileadmin');
const [exporters, profiles] = responses;
this.setState({
exporters,
canAdmin,
profiles: sortProfiles(profiles),
loading: false
});
@@ -77,12 +73,14 @@ export default class App extends React.Component {

const finalLanguages = Object.values(this.props.languages);

const canAdmin = this.props.currentUser.permissions.global.includes('profileadmin');

return React.cloneElement(this.props.children, {
profiles: this.state.profiles,
languages: finalLanguages,
exporters: this.state.exporters,
canAdmin: this.state.canAdmin,
updateProfiles: this.updateProfiles
updateProfiles: this.updateProfiles,
canAdmin
});
}


+ 2
- 1
server/sonar-web/src/main/js/apps/quality-profiles/components/AppContainer.js View File

@@ -19,10 +19,11 @@
*/
import { connect } from 'react-redux';
import App from './App';
import { getLanguages } from '../../../app/store/rootReducer';
import { getLanguages, getCurrentUser } from '../../../app/store/rootReducer';

export default connect(
state => ({
currentUser: getCurrentUser(state),
languages: getLanguages(state)
})
)(App);

+ 102
- 0
server/sonar-web/src/main/js/apps/sessions/components/LoginForm.js View File

@@ -0,0 +1,102 @@
/*
* 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.
*/
// @flow
import React from 'react';
import GlobalMessagesContainer from '../../../app/components/GlobalMessagesContainer';
import { translate } from '../../../helpers/l10n';

export default class LoginForm extends React.Component {
static propTypes = {
identityProviders: React.PropTypes.array.isRequired,
onSubmit: React.PropTypes.func.isRequired
};

state = {
login: '',
password: ''
};

handleSubmit = (e: any) => {
e.preventDefault();
this.props.onSubmit(this.state.login, this.state.password);
};

render () {
return (
<div>
<h1 className="maintenance-title text-center">Log In to SonarQube</h1>

{this.props.identityProviders.length > 0 && (
<section className="oauth-providers">
<ul>
{this.props.identityProviders.map(identityProvider => (
<li key={identityProvider.key}>
<a href={`${window.baseUrl}/sessions/init/${identityProvider.key}`}
style={{ backgroundColor: identityProvider.backgroundColor }}
title={`Log in with ${identityProvider.name}` }>
<img alt={identityProvider.name} width="20" height="20"
src={window.baseUrl + identityProvider.iconPath}/>
<span>Log in with {identityProvider.name}</span>
</a>
</li>
))}
</ul>
</section>
)}

<form id="login_form" onSubmit={this.handleSubmit}>
<GlobalMessagesContainer/>

<div className="big-spacer-bottom">
<label htmlFor="login" className="login-label">{translate('login')}</label>
<input type="text"
id="login"
name="login"
className="login-input"
maxLength="255"
required={true}
placeholder={translate('login')}
value={this.state.login}
onChange={e => this.setState({ login: e.target.value })}/>
</div>

<div className="big-spacer-bottom">
<label htmlFor="password" className="login-label">{translate('password')}</label>
<input type="password"
id="password"
name="password"
className="login-input"
required={true}
placeholder={translate('password')}
value={this.state.password}
onChange={e => this.setState({ password: e.target.value })}/>
</div>

<div>
<div className="text-right overflow-hidden">
<button name="commit" type="submit">{translate('sessions.log_in')}</button>
<a className="spacer-left" href={window.baseUrl + '/'}>{translate('cancel')}</a>
</div>
</div>
</form>
</div>
);
}
}

+ 78
- 0
server/sonar-web/src/main/js/apps/sessions/components/LoginFormContainer.js View File

@@ -0,0 +1,78 @@
/*
* 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.
*/
// @flow
import React from 'react';
import { connect } from 'react-redux';
import LoginForm from './LoginForm';
import { doLogin } from '../../../app/store/rootActions';
import { getAppState } from '../../../app/store/rootReducer';
import { getIdentityProviders } from '../../../api/users';

class LoginFormContainer extends React.Component {
mounted: bool;

static propTypes = {
location: React.PropTypes.object.isRequired
};

state = {};

componentDidMount () {
this.mounted = true;
getIdentityProviders().then(r => {
if (this.mounted) {
this.setState({ identityProviders: r.identityProviders });
}
});
}

componentWillUnmount () {
this.mounted = false;
}

handleSuccessfulLogin = () => {
window.location = this.props.location.query['return_to'] || (window.baseUrl + '/');
};

handleSubmit = (login: string, password: string) => {
this.props.doLogin(login, password).then(
this.handleSuccessfulLogin,
() => { /* do nothing */ }
);
};

render () {
if (!this.state.identityProviders) {
return null;
}

return (
<LoginForm identityProviders={this.state.identityProviders} onSubmit={this.handleSubmit}/>
);
}
}

const mapStateToProps = state => ({
appState: getAppState(state)
});

const mapDispatchToProps = { doLogin };

export default connect(mapStateToProps, mapDispatchToProps)(LoginFormContainer);

+ 42
- 0
server/sonar-web/src/main/js/apps/sessions/components/Logout.js View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
// @flow
import React from 'react';
import { connect } from 'react-redux';
import GlobalMessagesContainer from '../../../app/components/GlobalMessagesContainer';
import { doLogout } from '../../../app/store/rootActions';

class Logout extends React.Component {
componentDidMount () {
this.props.doLogout()
.then(() => window.location = window.baseUrl + '/')
.catch(() => { /* do nothing */ });
}

render () {
return <GlobalMessagesContainer/>;
}
}

const mapStateToProps = () => ({});

const mapDispatchToProps = { doLogout };

export default connect(mapStateToProps, mapDispatchToProps)(Logout);

+ 49
- 0
server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.js View File

@@ -0,0 +1,49 @@
/*
* 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.
*/
// @flow
import React from 'react';

export default class Unauthorized extends React.Component {
static propTypes = {
location: React.PropTypes.object.isRequired
};

render () {
const { message } = this.props.location.query;

return (
<div className="text-center">
<p id="unauthorized">
You're not authorized to access this page. Please contact the administrator.
</p>

{!!message && (
<p className="spacer-top">
Reason : {message}
</p>
)}

<div className="big-spacer-top">
<a href={window.baseUrl + '/'}>Home</a>
</div>
</div>
);
}
}

+ 31
- 0
server/sonar-web/src/main/js/apps/sessions/routes.js View File

@@ -0,0 +1,31 @@
/*
* 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 { Route, Redirect } from 'react-router';
import LoginFormContainer from './components/LoginFormContainer';
import Logout from './components/Logout';
import Unauthorized from './components/Unauthorized';

export default [
<Redirect key="login" from="/sessions/login" to="/sessions/new"/>,
<Route key="new" path="new" component={LoginFormContainer}/>,
<Route key="logout" path="logout" component={Logout}/>,
<Route key="unauthorized" path="unauthorized" component={Unauthorized}/>,
];

+ 6
- 18
server/sonar-web/src/main/js/apps/settings/components/AppContainer.js View File

@@ -17,24 +17,12 @@
* 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 { connect } from 'react-redux';
import App from './App';
import { getComponent } from '../../../app/store/rootReducer';

export default class AppContainer extends React.Component {
state = {};
const mapStateToProps = (state, ownProps) => ({
component: ownProps.location.query.id ? getComponent(state, ownProps.location.query.id) : undefined
});

componentDidMount () {
window.sonarqube.appStarted.then(options =>
this.setState({ ready: true, component: options.component }));
}

render () {
if (!this.state.ready) {
return null;
}

return (
<App {...this.props} component={this.state.component}/>
);
}
}
export default connect(mapStateToProps)(App);

+ 10
- 2
server/sonar-web/src/main/js/apps/settings/components/EmailForm.js View File

@@ -18,15 +18,17 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { connect } from 'react-redux';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { sendTestEmail } from '../../../api/settings';
import { parseError } from '../../code/utils';
import { getCurrentUser } from '../../../app/store/rootReducer';

export default class EmailForm extends React.Component {
class EmailForm extends React.Component {
constructor (props) {
super(props);
this.state = {
recipient: window.SS.userEmail,
recipient: this.props.currentUser.email,
subject: translate('email_configuration.test.subject'),
message: translate('email_configuration.test.message_text'),
loading: false,
@@ -114,3 +116,9 @@ export default class EmailForm extends React.Component {
);
}
}

const mapStateToProps = state => ({
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(EmailForm);

+ 0
- 0
server/sonar-web/src/main/js/apps/settings/store/values/reducer.js View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save