From f1976a3f56f03c67ccbf6dca7ee5060b6a21a1da Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 29 Nov 2016 17:03:24 +0100 Subject: [PATCH] SONAR-8451 Run js app outside of ruby container --- .../java/it/issue/IssueNotificationsTest.java | 2 + .../test/java/it/issue/IssueSearchTest.java | 1 + .../ProjectAdministrationTest.java | 2 +- .../test/java/it/projectEvent/EventTest.java | 1 + .../QualityGateNotificationTest.java | 2 + .../it/qualityGate/QualityGateUiTest.java | 2 + .../java/it/uiExtension/UiExtensionsTest.java | 3 + .../java/it/user/LocalAuthenticationTest.java | 1 + .../java/it/user/SsoAuthenticationTest.java | 6 +- .../updateCenter/installed-plugins.html | 12 +- .../force-authentication.html | 11 - ..._to_original_url_after_indirect_login.html | 5 - .../config/webpack/webpack.config.base.js | 2 - server/sonar-web/public/index.html | 2 +- server/sonar-web/src/main/js/api/auth.js | 7 + server/sonar-web/src/main/js/api/settings.js | 5 + server/sonar-web/src/main/js/api/system.js | 4 +- .../components/AdminContainer.js} | 44 +- .../src/main/js/app/components/App.js | 42 +- .../links-mixin.js => GlobalContainer.js} | 31 +- .../main/js/app/components/GlobalFooter.js | 81 ++++ .../main/js/app/components/GlobalLoading.css | 19 + .../main/js/app/components/GlobalLoading.js | 33 ++ .../src/main/js/app/components/Landing.js | 49 ++ .../app/components/LocalizationContainer.js | 49 ++ .../js/app/components/MigrationContainer.js | 52 +++ .../src/main/js/app/components/NotFound.js | 33 ++ .../js/app/components/ProjectContainer.js | 64 +++ .../main/js/app/components/SimpleContainer.js | 55 +++ .../src/main/js/app/components/nav/app.js | 91 ---- .../components/nav/component/ComponentNav.css | 16 + .../{component-nav.js => ComponentNav.js} | 45 +- ...adcrumbs.js => ComponentNavBreadcrumbs.js} | 12 +- ...av-favorite.js => ComponentNavFavorite.js} | 11 +- ...ponent-nav-menu.js => ComponentNavMenu.js} | 120 ++--- ...ponent-nav-meta.js => ComponentNavMeta.js} | 0 .../ComponentNavBreadcrumbs-test.js} | 14 +- .../global/{global-nav.js => GlobalNav.js} | 66 +-- ...l-nav-branding.js => GlobalNavBranding.js} | 39 +- .../{global-nav-menu.js => GlobalNavMenu.js} | 48 +- ...lobal-nav-search.js => GlobalNavSearch.js} | 50 +- .../{global-nav-user.js => GlobalNavUser.js} | 46 +- .../global/{search-view.js => SearchView.js} | 5 +- ...cuts-help-view.js => ShortcutsHelpView.js} | 0 .../components/nav/settings/SettingsNav.js | 134 ++++++ .../components/nav/settings/settings-nav.js | 133 ------ server/sonar-web/src/main/js/app/index.js | 14 +- .../src/main/js/app/store/appState/duck.js | 75 +++ .../src/main/js/app/store/rootActions.js | 51 +- .../src/main/js/app/store/rootReducer.js | 10 + .../src/main/js/app/store/users/actions.js | 6 +- .../src/main/js/app/store/users/reducer.js | 4 +- .../getCurrentUserFromStore.js} | 20 +- .../src/main/js/app/utils/getHistory.js | 35 ++ .../src/main/js/app/utils/getStore.js | 33 ++ .../app/utils/handleRequiredAuthentication.js | 36 ++ .../app/utils/handleRequiredAuthorization.js | 36 ++ .../handleRequiredMigration.js} | 7 +- .../main/js/app/utils/isCurrentPathKnown.js | 69 --- .../main/js/app/utils/startAjaxMonitoring.js | 7 + .../src/main/js/app/utils/startApp.js | 69 --- .../src/main/js/app/utils/startReactApp.js | 125 ++--- .../main/js/apps/about/components/AboutApp.js | 44 +- .../js/apps/account/components/Account.js | 10 - .../components/BackgroundTasksApp.js | 13 +- .../src/main/js/apps/code/components/App.js | 12 +- .../components/ComponentIssuesAppContainer.js | 13 +- .../src/main/js/apps/component-issues/init.js | 28 +- .../component-measures/app/AppContainer.js | 11 +- .../component/components/App.js} | 19 +- .../src/main/js/apps/component/routes.js | 28 ++ .../components/CustomMeasuresAppContainer.js | 20 +- .../src/main/js/apps/issues/HeaderView.js | 2 +- .../issues/components/IssuesAppContainer.js | 16 +- .../sonar-web/src/main/js/apps/issues/init.js | 8 +- .../main/js/apps/overview/components/App.js | 10 +- .../apps/overview/components/AppContainer.js | 32 +- .../src/main/js/apps/overview/meta/Meta.js | 10 +- .../apps/overview/meta/MetaQualityProfiles.js | 15 +- .../components/AppContainer.js | 28 +- .../permissions/project/components/App.js | 14 +- .../project/components/PageHeader.js | 3 +- .../apps/project-admin/deletion/Deletion.js | 10 +- .../src/main/js/apps/project-admin/key/Key.js | 5 +- .../main/js/apps/project-admin/links/Links.js | 5 +- .../project-admin/quality-gate/QualityGate.js | 5 +- .../quality-profiles/QualityProfiles.js | 9 +- .../js/apps/projects-admin/AppContainer.js | 20 +- .../apps/projects/components/AllProjects.js | 4 - .../projects/components/FavoriteFilter.js | 2 +- .../apps/quality-profiles/components/App.js | 14 +- .../components/AppContainer.js | 3 +- .../js/apps/sessions/components/LoginForm.js | 102 ++++ .../sessions/components/LoginFormContainer.js | 78 ++++ .../js/apps/sessions/components/Logout.js | 42 ++ .../apps/sessions/components/Unauthorized.js | 49 ++ .../src/main/js/apps/sessions/routes.js | 31 ++ .../apps/settings/components/AppContainer.js | 24 +- .../js/apps/settings/components/EmailForm.js | 12 +- .../js/apps/settings/store/values/reducer.js | 14 +- .../src/main/js/apps/settings/types.js | 1 + .../src/main/js/apps/source-viewer/app.js | 49 -- .../components/UpdateCenterAppContainer.js | 12 +- .../src/main/js/apps/update-center/init.js | 14 +- .../js/apps/users/change-password-view.js | 2 +- .../users/components/UsersAppContainer.js | 16 +- .../sonar-web/src/main/js/apps/users/init.js | 12 +- .../src/main/js/apps/users/list-item-view.js | 3 +- .../src/main/js/apps/users/list-view.js | 5 +- .../main/js/components/issue/issue-view.js | 4 +- .../issue/views/assign-form-view.js | 4 +- .../navigator/filters/ajax-select-filters.js | 437 ------------------ .../navigator/filters/base-filters.js | 227 --------- .../navigator/filters/checkbox-filters.js | 65 --- .../navigator/filters/choice-filters.js | 383 --------------- .../navigator/filters/favorite-filters.js | 91 ---- .../navigator/filters/filter-bar.js | 179 ------- .../navigator/filters/metric-filters.js | 204 -------- .../filters/more-criteria-filters.js | 107 ----- .../navigator/filters/range-filters.js | 208 --------- .../navigator/filters/string-filters.js | 87 ---- .../js/components/store/globalMessages.js | 19 + .../src/main/js/components/ui/Avatar.js | 20 +- .../js/components/ui/__tests__/Avatar-test.js | 24 +- .../js/helpers/__tests__/measures-test.js | 1 - .../sonar-web/src/main/js/helpers/cookies.js | 3 +- server/sonar-web/src/main/js/helpers/csv.js | 3 +- .../js/helpers/handlebars/avatarHelper.js | 10 +- .../js/helpers/handlebars/ifShowAvatars.js | 11 +- server/sonar-web/src/main/js/helpers/l10n.js | 6 +- .../sonar-web/src/main/js/helpers/measures.js | 26 +- .../sonar-web/src/main/js/helpers/request.js | 65 ++- .../ifCanUseFilter.js => users.js} | 12 +- server/sonar-web/src/main/js/libs/sonar.js | 50 -- .../src/main/less/components/page.less | 2 +- 135 files changed, 2109 insertions(+), 3065 deletions(-) rename server/sonar-web/src/main/js/{apps/about/components/LoginSection.js => app/components/AdminContainer.js} (53%) rename server/sonar-web/src/main/js/app/components/{nav/links-mixin.js => GlobalContainer.js} (61%) create mode 100644 server/sonar-web/src/main/js/app/components/GlobalFooter.js create mode 100644 server/sonar-web/src/main/js/app/components/GlobalLoading.css create mode 100644 server/sonar-web/src/main/js/app/components/GlobalLoading.js create mode 100644 server/sonar-web/src/main/js/app/components/Landing.js create mode 100644 server/sonar-web/src/main/js/app/components/LocalizationContainer.js create mode 100644 server/sonar-web/src/main/js/app/components/MigrationContainer.js create mode 100644 server/sonar-web/src/main/js/app/components/NotFound.js create mode 100644 server/sonar-web/src/main/js/app/components/ProjectContainer.js create mode 100644 server/sonar-web/src/main/js/app/components/SimpleContainer.js delete mode 100644 server/sonar-web/src/main/js/app/components/nav/app.js create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css rename server/sonar-web/src/main/js/app/components/nav/component/{component-nav.js => ComponentNav.js} (65%) rename server/sonar-web/src/main/js/app/components/nav/component/{component-nav-breadcrumbs.js => ComponentNavBreadcrumbs.js} (90%) rename server/sonar-web/src/main/js/app/components/nav/component/{component-nav-favorite.js => ComponentNavFavorite.js} (87%) rename server/sonar-web/src/main/js/app/components/nav/component/{component-nav-menu.js => ComponentNavMenu.js} (85%) rename server/sonar-web/src/main/js/app/components/nav/component/{component-nav-meta.js => ComponentNavMeta.js} (100%) rename server/sonar-web/src/main/js/app/components/nav/{__tests__/nav-test.js => component/__tests__/ComponentNavBreadcrumbs-test.js} (66%) rename server/sonar-web/src/main/js/app/components/nav/global/{global-nav.js => GlobalNav.js} (54%) rename server/sonar-web/src/main/js/app/components/nav/global/{global-nav-branding.js => GlobalNavBranding.js} (52%) rename server/sonar-web/src/main/js/app/components/nav/global/{global-nav-menu.js => GlobalNavMenu.js} (80%) rename server/sonar-web/src/main/js/app/components/nav/global/{global-nav-search.js => GlobalNavSearch.js} (80%) rename server/sonar-web/src/main/js/app/components/nav/global/{global-nav-user.js => GlobalNavUser.js} (81%) rename server/sonar-web/src/main/js/app/components/nav/global/{search-view.js => SearchView.js} (98%) rename server/sonar-web/src/main/js/app/components/nav/global/{shortcuts-help-view.js => ShortcutsHelpView.js} (100%) create mode 100644 server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js delete mode 100644 server/sonar-web/src/main/js/app/components/nav/settings/settings-nav.js create mode 100644 server/sonar-web/src/main/js/app/store/appState/duck.js rename server/sonar-web/src/main/js/app/{components/nav/dashboard-name-mixin.js => utils/getCurrentUserFromStore.js} (73%) create mode 100644 server/sonar-web/src/main/js/app/utils/getHistory.js create mode 100644 server/sonar-web/src/main/js/app/utils/getStore.js create mode 100644 server/sonar-web/src/main/js/app/utils/handleRequiredAuthentication.js create mode 100644 server/sonar-web/src/main/js/app/utils/handleRequiredAuthorization.js rename server/sonar-web/src/main/js/app/{components/NullComponent.js => utils/handleRequiredMigration.js} (90%) delete mode 100644 server/sonar-web/src/main/js/app/utils/isCurrentPathKnown.js delete mode 100644 server/sonar-web/src/main/js/app/utils/startApp.js rename server/sonar-web/src/main/js/{app/components/ComponentContainer.js => apps/component/components/App.js} (68%) create mode 100644 server/sonar-web/src/main/js/apps/component/routes.js create mode 100644 server/sonar-web/src/main/js/apps/sessions/components/LoginForm.js create mode 100644 server/sonar-web/src/main/js/apps/sessions/components/LoginFormContainer.js create mode 100644 server/sonar-web/src/main/js/apps/sessions/components/Logout.js create mode 100644 server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.js create mode 100644 server/sonar-web/src/main/js/apps/sessions/routes.js delete mode 100644 server/sonar-web/src/main/js/apps/source-viewer/app.js delete mode 100644 server/sonar-web/src/main/js/components/navigator/filters/ajax-select-filters.js delete mode 100644 server/sonar-web/src/main/js/components/navigator/filters/base-filters.js delete mode 100644 server/sonar-web/src/main/js/components/navigator/filters/checkbox-filters.js delete mode 100644 server/sonar-web/src/main/js/components/navigator/filters/choice-filters.js delete mode 100644 server/sonar-web/src/main/js/components/navigator/filters/favorite-filters.js delete mode 100644 server/sonar-web/src/main/js/components/navigator/filters/filter-bar.js delete mode 100644 server/sonar-web/src/main/js/components/navigator/filters/metric-filters.js delete mode 100644 server/sonar-web/src/main/js/components/navigator/filters/more-criteria-filters.js delete mode 100644 server/sonar-web/src/main/js/components/navigator/filters/range-filters.js delete mode 100644 server/sonar-web/src/main/js/components/navigator/filters/string-filters.js rename server/sonar-web/src/main/js/helpers/{handlebars/ifCanUseFilter.js => users.js} (83%) delete mode 100644 server/sonar-web/src/main/js/libs/sonar.js diff --git a/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java b/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java index 666909345a9..4b81c107baf 100644 --- a/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java +++ b/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java @@ -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"; diff --git a/it/it-tests/src/test/java/it/issue/IssueSearchTest.java b/it/it-tests/src/test/java/it/issue/IssueSearchTest.java index f5d0852b953..6d56d27d45b 100644 --- a/it/it-tests/src/test/java/it/issue/IssueSearchTest.java +++ b/it/it-tests/src/test/java/it/issue/IssueSearchTest.java @@ -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"); } diff --git a/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java b/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java index 4e5b12fd292..044e342a07c 100644 --- a/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java +++ b/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java @@ -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")) diff --git a/it/it-tests/src/test/java/it/projectEvent/EventTest.java b/it/it-tests/src/test/java/it/projectEvent/EventTest.java index 47aa6fd00ce..062e2666966 100644 --- a/it/it-tests/src/test/java/it/projectEvent/EventTest.java +++ b/it/it-tests/src/test/java/it/projectEvent/EventTest.java @@ -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 diff --git a/it/it-tests/src/test/java/it/qualityGate/QualityGateNotificationTest.java b/it/it-tests/src/test/java/it/qualityGate/QualityGateNotificationTest.java index ef7357c753d..2326c8ec0d4 100644 --- a/it/it-tests/src/test/java/it/qualityGate/QualityGateNotificationTest.java +++ b/it/it-tests/src/test/java/it/qualityGate/QualityGateNotificationTest.java @@ -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 { diff --git a/it/it-tests/src/test/java/it/qualityGate/QualityGateUiTest.java b/it/it-tests/src/test/java/it/qualityGate/QualityGateUiTest.java index 503bc19c535..ef5fdf25a82 100644 --- a/it/it-tests/src/test/java/it/qualityGate/QualityGateUiTest.java +++ b/it/it-tests/src/test/java/it/qualityGate/QualityGateUiTest.java @@ -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"); diff --git a/it/it-tests/src/test/java/it/uiExtension/UiExtensionsTest.java b/it/it-tests/src/test/java/it/uiExtension/UiExtensionsTest.java index 2645493bfed..1502559561f 100644 --- a/it/it-tests/src/test/java/it/uiExtension/UiExtensionsTest.java +++ b/it/it-tests/src/test/java/it/uiExtension/UiExtensionsTest.java @@ -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"); } diff --git a/it/it-tests/src/test/java/it/user/LocalAuthenticationTest.java b/it/it-tests/src/test/java/it/user/LocalAuthenticationTest.java index 1aace64487e..1680459ab1b 100644 --- a/it/it-tests/src/test/java/it/user/LocalAuthenticationTest.java +++ b/it/it-tests/src/test/java/it/user/LocalAuthenticationTest.java @@ -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"); diff --git a/it/it-tests/src/test/java/it/user/SsoAuthenticationTest.java b/it/it-tests/src/test/java/it/user/SsoAuthenticationTest.java index bf147c5fd6b..ac17347a99c 100644 --- a/it/it-tests/src/test/java/it/user/SsoAuthenticationTest.java +++ b/it/it-tests/src/test/java/it/user/SsoAuthenticationTest.java @@ -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 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); } diff --git a/it/it-tests/src/test/resources/updateCenter/installed-plugins.html b/it/it-tests/src/test/resources/updateCenter/installed-plugins.html index 01c518c4f9f..94db2c3521a 100644 --- a/it/it-tests/src/test/resources/updateCenter/installed-plugins.html +++ b/it/it-tests/src/test/resources/updateCenter/installed-plugins.html @@ -15,7 +15,12 @@ open - /updatecenter + /sessions/login + + + + waitForElementPresent + name=commit @@ -38,6 +43,11 @@ css=.js-user-authenticated + + open + /updatecenter + + waitForText content diff --git a/it/it-tests/src/test/resources/user/LocalAuthenticationTest/force-authentication.html b/it/it-tests/src/test/resources/user/LocalAuthenticationTest/force-authentication.html index a2f80c42291..1b542e5360c 100644 --- a/it/it-tests/src/test/resources/user/LocalAuthenticationTest/force-authentication.html +++ b/it/it-tests/src/test/resources/user/LocalAuthenticationTest/force-authentication.html @@ -23,11 +23,6 @@ / - - assertLocation - */sessions/new - - waitForText content @@ -63,12 +58,6 @@ /sessions/logout - - assertLocation - */sessions/new - - - diff --git a/it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html b/it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html index 9683c351923..3d27ac4b5e3 100644 --- a/it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html +++ b/it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html @@ -17,11 +17,6 @@ /settings - - assertLocation - */sessions/new - - waitForText content diff --git a/server/sonar-web/config/webpack/webpack.config.base.js b/server/sonar-web/config/webpack/webpack.config.base.js index 1d09d656426..f6551d344d7 100644 --- a/server/sonar-web/config/webpack/webpack.config.base.js +++ b/server/sonar-web/config/webpack/webpack.config.base.js @@ -20,8 +20,6 @@ module.exports = { 'handlebars/runtime' ], - 'sonar': './src/main/js/libs/sonar.js', - 'app': './src/main/js/app/index.js' }, output: { diff --git a/server/sonar-web/public/index.html b/server/sonar-web/public/index.html index cf8fd6ce5a4..a0acf459721 100644 --- a/server/sonar-web/public/index.html +++ b/server/sonar-web/public/index.html @@ -3,7 +3,7 @@ - + <% for (var css in htmlWebpackPlugin.files.css) { %> <% } %> diff --git a/server/sonar-web/src/main/js/api/auth.js b/server/sonar-web/src/main/js/api/auth.js index 064ca296aaf..5781eb6c4fd 100644 --- a/server/sonar-web/src/main/js/api/auth.js +++ b/server/sonar-web/src/main/js/api/auth.js @@ -36,3 +36,10 @@ export const login = (login, password) => ( .submit() .then(basicCheckStatus) ); + +export const logout = () => ( + request('/api/authentication/logout') + .setMethod('POST') + .submit() + .then(basicCheckStatus) +); diff --git a/server/sonar-web/src/main/js/api/settings.js b/server/sonar-web/src/main/js/api/settings.js index b35a877809e..50b3d8e1898 100644 --- a/server/sonar-web/src/main/js/api/settings.js +++ b/server/sonar-web/src/main/js/api/settings.js @@ -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) +); diff --git a/server/sonar-web/src/main/js/api/system.js b/server/sonar-web/src/main/js/api/system.js index fcd5d791f9b..905c1eef85f 100644 --- a/server/sonar-web/src/main/js/api/system.js +++ b/server/sonar-web/src/main/js/api/system.js @@ -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(); diff --git a/server/sonar-web/src/main/js/apps/about/components/LoginSection.js b/server/sonar-web/src/main/js/app/components/AdminContainer.js similarity index 53% rename from server/sonar-web/src/main/js/apps/about/components/LoginSection.js rename to server/sonar-web/src/main/js/app/components/AdminContainer.js index 08773d996bd..7ecbb77b2cd 100644 --- a/server/sonar-web/src/main/js/apps/about/components/LoginSection.js +++ b/server/sonar-web/src/main/js/app/components/AdminContainer.js @@ -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 ( -
-
- {authProviders.map(provider => ( - - ))} - - - - {loginWithSonarQubeLabel} - -
+
+ + {this.props.children}
); } } + +const mapStateToProps = state => ({ + currentUser: getCurrentUser(state) +}); + +export default connect(mapStateToProps)(AdminContainer); diff --git a/server/sonar-web/src/main/js/app/components/App.js b/server/sonar-web/src/main/js/app/components/App.js index c630a4fe275..4c13709deaf 100644 --- a/server/sonar-web/src/main/js/app/components/App.js +++ b/server/sonar-web/src/main/js/app/components/App.js @@ -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 ; + } + return this.props.children; } } export default connect( - () => ({}), - { fetchCurrentUser, fetchLanguages } + null, + { fetchAppState, fetchCurrentUser, fetchLanguages } )(App); diff --git a/server/sonar-web/src/main/js/app/components/nav/links-mixin.js b/server/sonar-web/src/main/js/app/components/GlobalContainer.js similarity index 61% rename from server/sonar-web/src/main/js/app/components/nav/links-mixin.js rename to server/sonar-web/src/main/js/app/components/GlobalContainer.js index 0027e2250df..447e647ffec 100644 --- a/server/sonar-web/src/main/js/app/components/nav/links-mixin.js +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.js @@ -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 ( -
  • - {title} -
  • +
    +
    + + + {this.props.children} +
    + +
    ); } -}; +} diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.js b/server/sonar-web/src/main/js/app/components/GlobalFooter.js new file mode 100644 index 00000000000..ac32f218810 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/GlobalFooter.js @@ -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 ( + + ); + } +} + +const mapStateToProps = state => ({ + sonarqubeVersion: getAppState(state).version, + productionDatabase: getAppState(state).productionDatabase +}); + +export default connect(mapStateToProps)(GlobalFooter); diff --git a/server/sonar-web/src/main/js/app/components/GlobalLoading.css b/server/sonar-web/src/main/js/app/components/GlobalLoading.css new file mode 100644 index 00000000000..5537f22c69a --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/GlobalLoading.css @@ -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; +} diff --git a/server/sonar-web/src/main/js/app/components/GlobalLoading.js b/server/sonar-web/src/main/js/app/components/GlobalLoading.js new file mode 100644 index 00000000000..535a20739d7 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/GlobalLoading.js @@ -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 ( +
    + + Loading... +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/Landing.js b/server/sonar-web/src/main/js/app/components/Landing.js new file mode 100644 index 00000000000..17074c24945 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/Landing.js @@ -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)); diff --git a/server/sonar-web/src/main/js/app/components/LocalizationContainer.js b/server/sonar-web/src/main/js/app/components/LocalizationContainer.js new file mode 100644 index 00000000000..1241ac8f20c --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/LocalizationContainer.js @@ -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; + } +} diff --git a/server/sonar-web/src/main/js/app/components/MigrationContainer.js b/server/sonar-web/src/main/js/app/components/MigrationContainer.js new file mode 100644 index 00000000000..ee6c7e49ad8 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/MigrationContainer.js @@ -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; + } +} diff --git a/server/sonar-web/src/main/js/app/components/NotFound.js b/server/sonar-web/src/main/js/app/components/NotFound.js new file mode 100644 index 00000000000..16b40224cef --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/NotFound.js @@ -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 ( + +

    The page you were looking for does not exist.

    +

    You may have mistyped the address or the page may have moved.

    +

    Go back to the homepage

    +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.js b/server/sonar-web/src/main/js/app/components/ProjectContainer.js new file mode 100644 index 00000000000..1a83c88e618 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/ProjectContainer.js @@ -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 ( +
    + {!isFile && ( + + )} + {this.props.children} +
    + ); + } +} + +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); diff --git a/server/sonar-web/src/main/js/app/components/SimpleContainer.js b/server/sonar-web/src/main/js/app/components/SimpleContainer.js new file mode 100644 index 00000000000..17d9c64cb69 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/SimpleContainer.js @@ -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 ( +
    +
    + + +
    +
    + {this.props.children} +
    +
    +
    + +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/nav/app.js b/server/sonar-web/src/main/js/app/components/nav/app.js deleted file mode 100644 index 2a5526bbbe3..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/app.js +++ /dev/null @@ -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(, 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(, 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(, el); - } - return r; - }); - } -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css new file mode 100644 index 00000000000..59d210c9534 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css @@ -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; +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/component-nav.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js similarity index 65% rename from server/sonar-web/src/main/js/app/components/nav/component/component-nav.js rename to server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js index bb511a169a5..b6dcd1c7359 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/component-nav.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js @@ -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 ( -
    - +
    + ); } }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-breadcrumbs.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js similarity index 90% rename from server/sonar-web/src/main/js/app/components/nav/component/component-nav-breadcrumbs.js rename to server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js index 7458095814c..248d96d7258 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-breadcrumbs.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js @@ -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({ ); }); + return (
      {items}
    ); } -}); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-favorite.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js similarity index 87% rename from server/sonar-web/src/main/js/app/components/nav/component/component-nav-favorite.js rename to server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js index 72b590e48fb..9e9cb8bfe1d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-favorite.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js @@ -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 (
    ); } -}); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-menu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js similarity index 85% rename from server/sonar-web/src/main/js/app/components/nav/component/component-nav-menu.js rename to server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js index e00f58efa03..dd16345bf43 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-menu.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js @@ -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 ( +
  • + {title} +
  • + ); + } + + renderDashboardLink () { const url = getComponentUrl(this.props.component.key); const name = ; const className = classNames({ active: this.isFixedDashboardActive() }); @@ -77,9 +81,9 @@ export default React.createClass({ {name} ); - }, + } - 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({ ); - }, + } - 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({ ); - }, + } - render() { + render () { return (
      {this.renderDashboardLink()} @@ -251,4 +255,4 @@ export default React.createClass({
    ); } -}); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-meta.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js similarity index 100% rename from server/sonar-web/src/main/js/app/components/nav/component/component-nav-meta.js rename to server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js diff --git a/server/sonar-web/src/main/js/app/components/nav/__tests__/nav-test.js b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js similarity index 66% rename from server/sonar-web/src/main/js/app/components/nav/__tests__/nav-test.js rename to server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js index d5ddbab8afd..4790e90ca41 100644 --- a/server/sonar-web/src/main/js/app/components/nav/__tests__/nav-test.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js @@ -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(); - 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(); + expect(result.find('li').length).toBe(1); + expect(result.find('a').length).toBe(1); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/global-nav.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js similarity index 54% rename from server/sonar-web/src/main/js/app/components/nav/global/global-nav.js rename to server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js index 1c2916c56de..01735c65a5a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/global-nav.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js @@ -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 ( -
    - +
    + ); } +} + +const mapStateToProps = state => ({ + currentUser: getCurrentUser(state) }); + +export default connect(mapStateToProps)(GlobalNav); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-branding.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.js similarity index 52% rename from server/sonar-web/src/main/js/app/components/nav/global/global-nav-branding.js rename to server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.js index 6241f7a4b63..6542a9fb20b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-branding.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.js @@ -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 {title}; - }, + return ( + {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 ( ); } +} + +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); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-menu.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js similarity index 80% rename from server/sonar-web/src/main/js/app/components/nav/global/global-nav-menu.js rename to server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js index 6295431ffb1..424ee074cef 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-menu.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js @@ -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 (
  • {translate('projects.page')}
  • ); - }, + } 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 (
  • {translate('issues.page')}
  • ); - }, + } renderRulesLink () { const url = window.baseUrl + '/coding_rules'; @@ -56,16 +62,16 @@ export default React.createClass({ {translate('coding_rules.page')} ); - }, + } - renderProfilesLink() { + renderProfilesLink () { const url = window.baseUrl + '/profiles'; return (
  • {translate('quality_profiles.page')}
  • ); - }, + } renderQualityGatesLink () { const url = window.baseUrl + '/quality_gates'; @@ -74,10 +80,10 @@ export default React.createClass({ {translate('quality_gates.page')} ); - }, + } 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({ {translate('layout.settings')} ); - }, + } renderGlobalPageLink (globalPage, index) { const url = window.baseUrl + globalPage.url; @@ -95,13 +101,13 @@ export default React.createClass({ {globalPage.name} ); - }, + } 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 (
  • @@ -113,7 +119,7 @@ export default React.createClass({
  • ); - }, + } render () { return ( @@ -128,4 +134,4 @@ export default React.createClass({ ); } -}); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-search.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js similarity index 80% rename from server/sonar-web/src/main/js/app/components/nav/global/global-nav-search.js rename to server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js index e84850bf365..c4d6f7c385c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-search.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js @@ -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 (
  • @@ -103,4 +103,10 @@ export default React.createClass({
  • ); } +} + +const mapStateToProps = state => ({ + currentUser: getCurrentUser(state) }); + +export default connect(mapStateToProps)(GlobalNavSearch); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-user.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js similarity index 81% rename from server/sonar-web/src/main/js/app/components/nav/global/global-nav-user.js rename to server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js index 8c2d0cc9949..11f633320c2 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-user.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js @@ -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 (
  • -   - {window.SS.userName}  +   + {currentUser.name} 
    • @@ -40,30 +53,17 @@ export default React.createClass({
  • ); - }, + } - renderAnonymous() { + renderAnonymous () { return (
  • {translate('layout.login')}
  • ); - }, - - 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(); } -}); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/search-view.js b/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js similarity index 98% rename from server/sonar-web/src/main/js/app/components/nav/global/search-view.js rename to server/sonar-web/src/main/js/app/components/nav/global/SearchView.js index 9974ddcc284..47d9fe6ae13 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/search-view.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/SearchView.js @@ -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) { diff --git a/server/sonar-web/src/main/js/app/components/nav/global/shortcuts-help-view.js b/server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelpView.js similarity index 100% rename from server/sonar-web/src/main/js/app/components/nav/global/shortcuts-help-view.js rename to server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelpView.js diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js new file mode 100644 index 00000000000..afe8b25f1f2 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js @@ -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 ( +
  • + {title} +
  • + ); + } + + 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 ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/settings-nav.js b/server/sonar-web/src/main/js/app/components/nav/settings/settings-nav.js deleted file mode 100644 index 087064ed84c..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/settings/settings-nav.js +++ /dev/null @@ -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 ( -
    -
      - {this.renderLink('/settings', translate('layout.settings'))} -
    - -
      -
    • - - {translate('sidebar.project_settings')} - {' '} - - -
        - {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))} -
      -
    • - -
    • - - {translate('sidebar.security')} - {' '} - - -
        - {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'))} -
      -
    • - -
    • - - {translate('sidebar.projects')} - {' '} - - -
        - {this.renderLink('/projects_admin', 'Management')} - {this.renderLink('/background_tasks', - translate('background_tasks.page'))} -
      -
    • - -
    • - - {translate('sidebar.system')} - {' '} - - -
        - {this.renderLink('/updatecenter', translate('update_center.page'))} - {this.renderLink('/system', translate('system_info.page'))} -
      -
    • -
    -
    - - ); - } -}); diff --git a/server/sonar-web/src/main/js/app/index.js b/server/sonar-web/src/main/js/app/index.js index 65145e45906..edca4bb505c 100644 --- a/server/sonar-web/src/main/js/app/index.js +++ b/server/sonar-web/src/main/js/app/index.js @@ -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(); diff --git a/server/sonar-web/src/main/js/app/store/appState/duck.js b/server/sonar-web/src/main/js/app/store/appState/duck.js new file mode 100644 index 00000000000..cd313870e19 --- /dev/null +++ b/server/sonar-web/src/main/js/app/store/appState/duck.js @@ -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 +}; + +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 +); diff --git a/server/sonar-web/src/main/js/app/store/rootActions.js b/server/sonar-web/src/main/js/app/store/rootActions.js index 92f6f4734cf..717c52f4345 100644 --- a/server/sonar-web/src/main/js/app/store/rootActions.js +++ b/server/sonar-web/src/main/js/app/store/rootActions.js @@ -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(); + } + ) +); diff --git a/server/sonar-web/src/main/js/app/store/rootReducer.js b/server/sonar-web/src/main/js/app/store/rootReducer.js index 396c2b5c5f8..b5cba2ff9bf 100644 --- a/server/sonar-web/src/main/js/app/store/rootReducer.js +++ b/server/sonar-web/src/main/js/app/store/rootReducer.js @@ -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) ); diff --git a/server/sonar-web/src/main/js/app/store/users/actions.js b/server/sonar-web/src/main/js/app/store/users/actions.js index 5ed397862c7..71e055d6526 100644 --- a/server/sonar-web/src/main/js/app/store/users/actions.js +++ b/server/sonar-web/src/main/js/app/store/users/actions.js @@ -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))) +); diff --git a/server/sonar-web/src/main/js/app/store/users/reducer.js b/server/sonar-web/src/main/js/app/store/users/reducer.js index d67d04bfdf7..03dd5f9df28 100644 --- a/server/sonar-web/src/main/js/app/store/users/reducer.js +++ b/server/sonar-web/src/main/js/app/store/users/reducer.js @@ -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 ); diff --git a/server/sonar-web/src/main/js/app/components/nav/dashboard-name-mixin.js b/server/sonar-web/src/main/js/app/utils/getCurrentUserFromStore.js similarity index 73% rename from server/sonar-web/src/main/js/app/components/nav/dashboard-name-mixin.js rename to server/sonar-web/src/main/js/app/utils/getCurrentUserFromStore.js index 2bc0227b02a..b16ba492a00 100644 --- a/server/sonar-web/src/main/js/app/components/nav/dashboard-name-mixin.js +++ b/server/sonar-web/src/main/js/app/utils/getCurrentUserFromStore.js @@ -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; diff --git a/server/sonar-web/src/main/js/app/utils/getHistory.js b/server/sonar-web/src/main/js/app/utils/getHistory.js new file mode 100644 index 00000000000..2f01de7566a --- /dev/null +++ b/server/sonar-web/src/main/js/app/utils/getHistory.js @@ -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() +); diff --git a/server/sonar-web/src/main/js/app/utils/getStore.js b/server/sonar-web/src/main/js/app/utils/getStore.js new file mode 100644 index 00000000000..1fb1cad4e4f --- /dev/null +++ b/server/sonar-web/src/main/js/app/utils/getStore.js @@ -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() +); diff --git a/server/sonar-web/src/main/js/app/utils/handleRequiredAuthentication.js b/server/sonar-web/src/main/js/app/utils/handleRequiredAuthentication.js new file mode 100644 index 00000000000..566434f196c --- /dev/null +++ b/server/sonar-web/src/main/js/app/utils/handleRequiredAuthentication.js @@ -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 } + }); +}; diff --git a/server/sonar-web/src/main/js/app/utils/handleRequiredAuthorization.js b/server/sonar-web/src/main/js/app/utils/handleRequiredAuthorization.js new file mode 100644 index 00000000000..b8664b312df --- /dev/null +++ b/server/sonar-web/src/main/js/app/utils/handleRequiredAuthorization.js @@ -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 } + }); +}; diff --git a/server/sonar-web/src/main/js/app/components/NullComponent.js b/server/sonar-web/src/main/js/app/utils/handleRequiredMigration.js similarity index 90% rename from server/sonar-web/src/main/js/app/components/NullComponent.js rename to server/sonar-web/src/main/js/app/utils/handleRequiredMigration.js index a3501dfbd16..1bed65c672e 100644 --- a/server/sonar-web/src/main/js/app/components/NullComponent.js +++ b/server/sonar-web/src/main/js/app/utils/handleRequiredMigration.js @@ -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'; +}; diff --git a/server/sonar-web/src/main/js/app/utils/isCurrentPathKnown.js b/server/sonar-web/src/main/js/app/utils/isCurrentPathKnown.js deleted file mode 100644 index 37468ca94f4..00000000000 --- a/server/sonar-web/src/main/js/app/utils/isCurrentPathKnown.js +++ /dev/null @@ -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); -} diff --git a/server/sonar-web/src/main/js/app/utils/startAjaxMonitoring.js b/server/sonar-web/src/main/js/app/utils/startAjaxMonitoring.js index cc3f75823db..94525d04434 100644 --- a/server/sonar-web/src/main/js/app/utils/startAjaxMonitoring.js +++ b/server/sonar-web/src/main/js/app/utils/startAjaxMonitoring.js @@ -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, diff --git a/server/sonar-web/src/main/js/app/utils/startApp.js b/server/sonar-web/src/main/js/app/utils/startApp.js deleted file mode 100644 index 9229702f16d..00000000000 --- a/server/sonar-web/src/main/js/app/utils/startApp.js +++ /dev/null @@ -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; - diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index 31b5aac4197..0d22ea04de8 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -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(( + + + + + {maintenanceRoutes} + {setupRoutes} + + + + + {sessionsRoutes} + - render(( - - - {aboutRoutes} - {accountRoutes} - {backgroundTasksRoutes} - {codingRulesRoutes} - {overviewRoutes} - {groupsRoutes} - {issuesRoutes} - {maintenanceRoutes} - {metricsRoutes} - {permissionTemplatesRoutes} - {projectsRoutes} - {projectsAdminRoutes} - {globalPermissionsRoutes} - {settingsRoutes} - {setupRoutes} - {systemRoutes} - {qualityGatesRoutes} - {qualityProfilesRoutes} - {updateCenterRoutes} - {usersRoutes} - {webAPIRoutes} - - {codeRoutes} - {componentIssuesRoutes} - {componentMeasuresRoutes} - {customMeasuresRoutes} - + + + + {aboutRoutes} + {accountRoutes} + {codingRulesRoutes} + {componentRoutes} + {issuesRoutes} + {projectsRoutes} + {qualityGatesRoutes} + {qualityProfilesRoutes} + {webAPIRoutes} + + + {codeRoutes} + {componentIssuesRoutes} + {componentMeasuresRoutes} + {customMeasuresRoutes} + {overviewRoutes} + + {backgroundTasksRoutes} + {settingsRoutes} + {projectAdminRoutes} + + {projectPermissionsRoutes} + + + {backgroundTasksRoutes} + {groupsRoutes} + {metricsRoutes} + {permissionTemplatesRoutes} + {projectsAdminRoutes} + {globalPermissionsRoutes} {settingsRoutes} - {projectAdminRoutes} + {systemRoutes} + {updateCenterRoutes} + {usersRoutes} - {projectPermissionsRoutes} - - - - - ), el); - }); - } + + + + + + + ), el); }; export default startReactApp; diff --git a/server/sonar-web/src/main/js/apps/about/components/AboutApp.js b/server/sonar-web/src/main/js/apps/about/components/AboutApp.js index dedff134191..dae2e6c8158 100644 --- a/server/sonar-web/src/main/js/apps/about/components/AboutApp.js +++ b/server/sonar-web/src/main/js/apps/about/components/AboutApp.js @@ -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 (
    @@ -113,8 +124,8 @@ export default class AboutApp extends React.Component {
    - {landingText.length > 0 && ( -
    + {customText != null && customText.length > 0 && ( +
    )}
    @@ -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); diff --git a/server/sonar-web/src/main/js/apps/account/components/Account.js b/server/sonar-web/src/main/js/apps/account/components/Account.js index 35e273a1d32..0606d1aef61 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Account.js +++ b/server/sonar-web/src/main/js/apps/account/components/Account.js @@ -28,16 +28,6 @@ class Account extends React.Component { render () { const { currentUser, children } = this.props; - if (currentUser == null) { - return ( -
    -
    - -
    -
    - ); - } - return (
    diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js index 0fa65014b61..94c5459e0fd 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.js index 7dc6b7158b3..fadc222005c 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.js +++ b/server/sonar-web/src/main/js/apps/code/components/App.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js b/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js index 430ba7b0e79..0b3278c596e 100644 --- a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js +++ b/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js @@ -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
    ; } } + +const mapStateToProps = (state, ownProps) => ({ + component: getComponent(state, ownProps.location.query.id), + currentUser: getCurrentUser(state) +}); + +export default connect(mapStateToProps)(ComponentIssuesAppContainer); diff --git a/server/sonar-web/src/main/js/apps/component-issues/init.js b/server/sonar-web/src/main/js/apps/component-issues/init.js index 95269406d71..9fd72073beb 100644 --- a/server/sonar-web/src/main/js/apps/component-issues/init.js +++ b/server/sonar-web/src/main/js/apps/component-issues/init.js @@ -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 }); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js index c3e9e632d10..5c1f09eaf63 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/app/AppContainer.js @@ -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 { diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.js b/server/sonar-web/src/main/js/apps/component/components/App.js similarity index 68% rename from server/sonar-web/src/main/js/app/components/ComponentContainer.js rename to server/sonar-web/src/main/js/apps/component/components/App.js index f31850c3122..fe7d0d59d80 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.js +++ b/server/sonar-web/src/main/js/apps/component/components/App.js @@ -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 ( +
    + +
    + ); } } diff --git a/server/sonar-web/src/main/js/apps/component/routes.js b/server/sonar-web/src/main/js/apps/component/routes.js new file mode 100644 index 00000000000..cee8c941699 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component/routes.js @@ -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 [ + , + +]; diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js b/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js index a88477e394a..ad4b91936c6 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js @@ -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
    ; } } + +const mapStateToProps = (state, ownProps) => ({ + component: getComponent(state, ownProps.location.query.id) +}); + +export default connect(mapStateToProps)(CustomMeasuresAppContainer); diff --git a/server/sonar-web/src/main/js/apps/issues/HeaderView.js b/server/sonar-web/src/main/js/apps/issues/HeaderView.js index 01d6e6cc546..1cb30562e74 100644 --- a/server/sonar-web/src/main/js/apps/issues/HeaderView.js +++ b/server/sonar-web/src/main/js/apps/issues/HeaderView.js @@ -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') }; } }); diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js index 33820285418..08cf0b30f15 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js @@ -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
    ; } } + +const mapStateToProps = state => ({ + currentUser: getCurrentUser(state) +}); + +export default connect(mapStateToProps)(IssuesAppContainer); diff --git a/server/sonar-web/src/main/js/apps/issues/init.js b/server/sonar-web/src/main/js/apps/issues/init.js index 2ac5041b6ef..da9b2134c07 100644 --- a/server/sonar-web/src/main/js/apps/issues/init.js +++ b/server/sonar-web/src/main/js/apps/issues/init.js @@ -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 }); } diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.js b/server/sonar-web/src/main/js/apps/overview/components/App.js index d791a021882..2aefdf292f3 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.js +++ b/server/sonar-web/src/main/js/apps/overview/components/App.js @@ -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 ( +
    + +
    + ); + } + if (!component.snapshotDate) { return ; } diff --git a/server/sonar-web/src/main/js/apps/overview/components/AppContainer.js b/server/sonar-web/src/main/js/apps/overview/components/AppContainer.js index 0e7bde7cdbd..e697ff24c18 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/overview/components/AppContainer.js @@ -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 ( - - ); - } -} +export default connect(mapStateToProps)(App); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js index 70d063d3101..c034da4452b 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js @@ -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 }) => { {shouldShowQualityGate && ( - + )} {shouldShowQualityProfiles && ( - + )} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js index 9500b14e6bd..5709d077ec1 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js @@ -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 = (
    - {'(' + profile.language + ')'} + {'(' + languageName + ')'} {profile.name} @@ -120,3 +125,9 @@ export default class MetaQualityProfiles extends React.Component { ); } } + +const mapStateToProps = state => ({ + languages: getLanguages(state) +}); + +export default connect(mapStateToProps)(MetaQualityProfiles); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js b/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js index b6f4134f066..4fb4138e7d2 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js @@ -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 ( - - ); - } -} +export default connect( + mapStateToProps +)(App); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js index ba2b4e44e2e..c7657ddcc51 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js @@ -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 (
    - +
    ); } } + +const mapStateToProps = (state, ownProps) => ({ + component: getComponent(state, ownProps.location.query.id), + currentUser: getCurrentUser(state) +}); + +export default connect(mapStateToProps)(App); + diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js index 14fcf947ee9..456a16cf7a3 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js @@ -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 { )} - {!!window.SS.isUserAdmin && ( + {isUserAdmin(this.props.currentUser) && (
    + {translate('cancel')} +
    +
    + +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/sessions/components/LoginFormContainer.js b/server/sonar-web/src/main/js/apps/sessions/components/LoginFormContainer.js new file mode 100644 index 00000000000..f3171181aed --- /dev/null +++ b/server/sonar-web/src/main/js/apps/sessions/components/LoginFormContainer.js @@ -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 ( + + ); + } +} + +const mapStateToProps = state => ({ + appState: getAppState(state) +}); + +const mapDispatchToProps = { doLogin }; + +export default connect(mapStateToProps, mapDispatchToProps)(LoginFormContainer); diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Logout.js b/server/sonar-web/src/main/js/apps/sessions/components/Logout.js new file mode 100644 index 00000000000..409a099aa18 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/sessions/components/Logout.js @@ -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 ; + } +} + +const mapStateToProps = () => ({}); + +const mapDispatchToProps = { doLogout }; + +export default connect(mapStateToProps, mapDispatchToProps)(Logout); diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.js b/server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.js new file mode 100644 index 00000000000..276c33809ec --- /dev/null +++ b/server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.js @@ -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 ( +
    +

    + You're not authorized to access this page. Please contact the administrator. +

    + + {!!message && ( +

    + Reason : {message} +

    + )} + +
    + Home +
    +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/sessions/routes.js b/server/sonar-web/src/main/js/apps/sessions/routes.js new file mode 100644 index 00000000000..98204df9838 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/sessions/routes.js @@ -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 [ + , + , + , + , +]; diff --git a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js index e4cd869edf9..288129ff5ec 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js @@ -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 ( - - ); - } -} +export default connect(mapStateToProps)(App); diff --git a/server/sonar-web/src/main/js/apps/settings/components/EmailForm.js b/server/sonar-web/src/main/js/apps/settings/components/EmailForm.js index d90c7fa0f28..3b236e0d52b 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/EmailForm.js +++ b/server/sonar-web/src/main/js/apps/settings/components/EmailForm.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js index 2369b75f3de..196687f245f 100644 --- a/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js +++ b/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js @@ -20,18 +20,24 @@ // @flow import keyBy from 'lodash/keyBy'; import { RECEIVE_VALUES } from './actions'; -import type { SettingValue } from '../../types'; +import { actions as appStateActions } from '../../../../app/store/appState/duck'; type State = { [key: string]: {} }; -type Action = { type: string, settings: SettingValue[] }; - -const reducer = (state: State = {}, action: Action) => { +const reducer = (state: State = {}, action: any) => { if (action.type === RECEIVE_VALUES) { const settingsByKey = keyBy(action.settings, 'key'); return { ...state, ...settingsByKey }; } + if (action.type === appStateActions.SET_APP_STATE) { + const settingsByKey = {}; + Object.keys(action.appState.settings).forEach(key => ( + settingsByKey[key] = { value: action.appState.settings[key] } + )); + return { ...state, ...settingsByKey }; + } + return state; }; diff --git a/server/sonar-web/src/main/js/apps/settings/types.js b/server/sonar-web/src/main/js/apps/settings/types.js index f82ae12a43b..65658d6987d 100644 --- a/server/sonar-web/src/main/js/apps/settings/types.js +++ b/server/sonar-web/src/main/js/apps/settings/types.js @@ -24,5 +24,6 @@ export type Definition = { }; export type SettingValue = { + key: string, value?: string }; diff --git a/server/sonar-web/src/main/js/apps/source-viewer/app.js b/server/sonar-web/src/main/js/apps/source-viewer/app.js deleted file mode 100644 index 700def699b4..00000000000 --- a/server/sonar-web/src/main/js/apps/source-viewer/app.js +++ /dev/null @@ -1,49 +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 Marionette from 'backbone.marionette'; -import SourceViewer from '../../components/source-viewer/main'; - -const App = new Marionette.Application(); -const init = function ({ el }) { - const options = window.sonarqube; - - this.addRegions({ mainRegion: window.sonarqube.el || el }); - - const viewer = new SourceViewer(); - this.mainRegion.show(viewer); - - if (typeof options.file.line === 'number') { - viewer.open(options.file.uuid, { aroundLine: options.file.line }); - viewer.on('loaded', function () { - viewer - .highlightLine(options.file.line) - .scrollToLine(options.file.line); - }); - } else { - viewer.open(options.file.uuid); - } -}; - -App.on('start', function (options) { - init.call(App, options); -}); - -window.sonarqube.appStarted.then(options => App.start(options)); - diff --git a/server/sonar-web/src/main/js/apps/update-center/components/UpdateCenterAppContainer.js b/server/sonar-web/src/main/js/apps/update-center/components/UpdateCenterAppContainer.js index 4e40999fe81..500e4c052fa 100644 --- a/server/sonar-web/src/main/js/apps/update-center/components/UpdateCenterAppContainer.js +++ b/server/sonar-web/src/main/js/apps/update-center/components/UpdateCenterAppContainer.js @@ -18,14 +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 { getSettingValue } from '../../../app/store/rootReducer'; -export default class UpdateCenterAppContainer extends React.Component { +class UpdateCenterAppContainer extends React.Component { componentDidMount () { - init(this.refs.container); + init(this.refs.container, this.props.updateCenterActive); } render () { return
    ; } } + +const mapStateToProps = state => ({ + updateCenterActive: (getSettingValue(state, 'sonar.updatecenter.activate') || {}).value +}); + +export default connect(mapStateToProps)(UpdateCenterAppContainer); diff --git a/server/sonar-web/src/main/js/apps/update-center/init.js b/server/sonar-web/src/main/js/apps/update-center/init.js index 3c509e36232..830a3555822 100644 --- a/server/sonar-web/src/main/js/apps/update-center/init.js +++ b/server/sonar-web/src/main/js/apps/update-center/init.js @@ -29,11 +29,9 @@ import Router from './router'; import Plugins from './plugins'; const App = new Marionette.Application(); -const init = function (el) { +const init = function ({ el, updateCenterActive }) { // State - this.state = new Backbone.Model({ - updateCenterActive: window.SS.updateCenterActive - }); + this.state = new Backbone.Model({ updateCenterActive }); // Layout this.layout = new Layout({ el }); @@ -72,10 +70,10 @@ const init = function (el) { }); }; -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, updateCenterActive) { + App.start({ el, updateCenterActive }); } diff --git a/server/sonar-web/src/main/js/apps/users/change-password-view.js b/server/sonar-web/src/main/js/apps/users/change-password-view.js index 120703690a1..fb8f29e3c04 100644 --- a/server/sonar-web/src/main/js/apps/users/change-password-view.js +++ b/server/sonar-web/src/main/js/apps/users/change-password-view.js @@ -53,7 +53,7 @@ export default ModalForm.extend({ serializeData () { return Object.assign({}, ModalForm.prototype.serializeData.apply(this, arguments), { - isOwnPassword: window.SS.user === this.model.id + isOwnPassword: this.options.currentUser.login === this.model.id }); } }); diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersAppContainer.js b/server/sonar-web/src/main/js/apps/users/components/UsersAppContainer.js index 8442a308327..8abe6fce357 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UsersAppContainer.js +++ b/server/sonar-web/src/main/js/apps/users/components/UsersAppContainer.js @@ -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 UsersAppContainer extends React.Component { + static propTypes = { + currentUser: React.PropTypes.object.isRequired + }; -export default class UsersAppContainer extends React.Component { componentDidMount () { - init(this.refs.container); + init(this.refs.container, this.props.currentUser); } render () { return
    ; } } + +const mapStateToProps = state => ({ + currentUser: getCurrentUser(state) +}); + +export default connect(mapStateToProps)(UsersAppContainer); diff --git a/server/sonar-web/src/main/js/apps/users/init.js b/server/sonar-web/src/main/js/apps/users/init.js index cd1dc8a6568..490ea231426 100644 --- a/server/sonar-web/src/main/js/apps/users/init.js +++ b/server/sonar-web/src/main/js/apps/users/init.js @@ -29,7 +29,7 @@ import { getIdentityProviders } from '../../api/users'; const App = new Marionette.Application(); -const init = function (el, providers) { +const init = function ({ el, currentUser }, providers) { // Layout this.layout = new Layout({ el }); this.layout.render(); @@ -46,7 +46,7 @@ const init = function (el, providers) { this.layout.searchRegion.show(this.searchView); // List View - this.listView = new ListView({ collection: this.users, providers }); + this.listView = new ListView({ collection: this.users, currentUser, providers }); this.layout.listRegion.show(this.listView); // List Footer View @@ -57,10 +57,10 @@ const init = function (el, providers) { this.users.fetch(); }; -App.on('start', function (el) { - getIdentityProviders().then(r => init.call(App, el, r.identityProviders)); +App.on('start', function (options) { + getIdentityProviders().then(r => init.call(App, options, r.identityProviders)); }); -export default function (el) { - App.start(el); +export default function (el, currentUser) { + App.start({ el, currentUser }); } diff --git a/server/sonar-web/src/main/js/apps/users/list-item-view.js b/server/sonar-web/src/main/js/apps/users/list-item-view.js index f879001e909..7ababc08609 100644 --- a/server/sonar-web/src/main/js/apps/users/list-item-view.js +++ b/server/sonar-web/src/main/js/apps/users/list-item-view.js @@ -109,7 +109,8 @@ export default Marionette.ItemView.extend({ changePassword () { new ChangePasswordView({ model: this.model, - collection: this.model.collection + collection: this.model.collection, + currentUser: this.options.currentUser }).render(); }, diff --git a/server/sonar-web/src/main/js/apps/users/list-view.js b/server/sonar-web/src/main/js/apps/users/list-view.js index 6fb416fc812..3f557d76db8 100644 --- a/server/sonar-web/src/main/js/apps/users/list-view.js +++ b/server/sonar-web/src/main/js/apps/users/list-view.js @@ -33,7 +33,10 @@ export default Marionette.CompositeView.extend({ }, childViewOptions () { - return { providers: this.options.providers }; + return { + providers: this.options.providers, + currentUser: this.options.currentUser + }; }, showLoading () { diff --git a/server/sonar-web/src/main/js/components/issue/issue-view.js b/server/sonar-web/src/main/js/components/issue/issue-view.js index 8b0d7493d8a..dcab97f7e5c 100644 --- a/server/sonar-web/src/main/js/components/issue/issue-view.js +++ b/server/sonar-web/src/main/js/components/issue/issue-view.js @@ -32,6 +32,7 @@ import SetTypeFormView from './views/set-type-form-view'; import TagsFormView from './views/tags-form-view'; import Workspace from '../workspace/main'; import Template from './templates/issue.hbs'; +import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore'; export default Marionette.ItemView.extend({ className: 'issue', @@ -213,7 +214,8 @@ export default Marionette.ItemView.extend({ model: this.model, triggerEl: $('body') }); - view.submit(window.SS.user, window.SS.userName); + const currentUser = getCurrentUserFromStore(); + view.submit(currentUser.login, currentUser.name); view.destroy(); }, diff --git a/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js b/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js index c947daedfe4..5a608cdecea 100644 --- a/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js +++ b/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js @@ -23,6 +23,7 @@ import ActionOptionsView from '../../common/action-options-view'; import Template from '../templates/issue-assign-form.hbs'; import OptionTemplate from '../templates/issue-assign-form-option.hbs'; import { translate } from '../../../helpers/l10n'; +import getCurrentUserFromStore from '../../../app/utils/getCurrentUserFromStore'; export default ActionOptionsView.extend({ template: Template, @@ -142,8 +143,9 @@ export default ActionOptionsView.extend({ if (this.assignees) { return this.assignees; } + const currentUser = getCurrentUserFromStore(); const assignees = [ - { id: window.SS.user, text: window.SS.userName }, + { id: currentUser.login, text: currentUser.name }, { id: '', text: translate('unassigned') } ]; return this.makeUnique(assignees); diff --git a/server/sonar-web/src/main/js/components/navigator/filters/ajax-select-filters.js b/server/sonar-web/src/main/js/components/navigator/filters/ajax-select-filters.js deleted file mode 100644 index cc4421cd3aa..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/filters/ajax-select-filters.js +++ /dev/null @@ -1,437 +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 'jquery'; -import _ from 'underscore'; -import Backbone from 'backbone'; -import BaseFilters from './base-filters'; -import ChoiceFilters from './choice-filters'; -import Template from '../templates/ajax-select-filter.hbs'; -import ListTemplate from '../templates/choice-filter-item.hbs'; - -const PAGE_SIZE = 100; - -const Suggestions = Backbone.Collection.extend({ - comparator: 'text', - - initialize () { - this.more = false; - this.page = 0; - }, - - parse (r) { - this.more = r.more; - return r.results; - }, - - fetch (options) { - this.data = _.extend({ - p: 1, - ps: PAGE_SIZE - }, options.data || {}); - - const settings = _.extend({}, options, { data: this.data }); - return Backbone.Collection.prototype.fetch.call(this, settings); - }, - - fetchNextPage (options) { - if (this.more) { - this.data.p += 1; - const settings = _.extend({ remove: false }, options, { data: this.data }); - return this.fetch(settings); - } - return false; - } - -}); - -const UserSuggestions = Suggestions.extend({ - - url () { - return window.baseUrl + '/api/users/search'; - }, - - parse (response) { - const parsedResponse = window.usersToSelect2(response); - this.more = parsedResponse.more; - this.results = parsedResponse.results; - } - -}); - -const ProjectSuggestions = Suggestions.extend({ - - url () { - return window.baseUrl + '/api/resources/search?f=s2&q=TRK&display_key=true'; - } - -}); - -const ComponentSuggestions = Suggestions.extend({ - - url () { - return window.baseUrl + '/api/resources/search?f=s2&qp=supportsGlobalDashboards&display_key=true'; - }, - - parse (r) { - this.more = r.more; - - // If results are divided into categories - if (r.results.length > 0 && r.results[0].children) { - const results = []; - _.each(r.results, function (category) { - _.each(category.children, function (child) { - child.category = category.text; - results.push(child); - }); - }); - return results; - } else { - return r.results; - } - } - -}); - -const AjaxSelectDetailsFilterView = ChoiceFilters.DetailsChoiceFilterView.extend({ - template: Template, - listTemplate: ListTemplate, - searchKey: 's', - - render () { - ChoiceFilters.DetailsChoiceFilterView.prototype.render.apply(this, arguments); - - const that = this; - const keyup = function (e) { - if (e.keyCode !== 37 && e.keyCode !== 38 && e.keyCode !== 39 && e.keyCode !== 40) { - that.search(); - } - }; - const debouncedKeyup = _.debounce(keyup, 250); - const scroll = function () { - that.scroll(); - }; - const throttledScroll = _.throttle(scroll, 1000); - - this.$('.navigator-filter-search input') - .off('keyup keydown') - .on('keyup', debouncedKeyup) - .on('keydown', this.keydown); - - this.$('.choices') - .off('scroll') - .on('scroll', throttledScroll); - }, - - search () { - const that = this; - this.query = this.$('.navigator-filter-search input').val(); - if (this.query.length > 1) { - this.$el.addClass('fetching'); - const selected = that.options.filterView.getSelected(); - const data = { ps: PAGE_SIZE }; - data[this.searchKey] = this.query; - this.options.filterView.choices.fetch({ - data, - success () { - selected.forEach(function (item) { - that.options.filterView.choices.unshift(item); - }); - _.each(that.model.get('choices'), function (v, k) { - if (k[0] === '!') { - that.options.filterView.choices.add(new Backbone.Model({ id: k, text: v })); - } - }); - that.updateLists(); - that.$el.removeClass('fetching'); - that.$('.navigator-filter-search').removeClass('fetching-error'); - }, - error () { - that.showSearchError(); - } - }); - } else { - this.resetChoices(); - this.updateLists(); - } - }, - - showSearchError () { - this.$el.removeClass('fetching'); - this.$('.navigator-filter-search').addClass('fetching-error'); - }, - - scroll () { - const that = this; - const el = this.$('.choices'); - const scrollBottom = el.scrollTop() >= el[0].scrollHeight - el.outerHeight(); - - if (scrollBottom) { - this.options.filterView.choices.fetchNextPage().done(function () { - that.updateLists(); - }); - } - }, - - keydown (e) { - if (_([38, 40, 13]).indexOf(e.keyCode) !== -1) { - e.preventDefault(); - } - }, - - resetChoices () { - const that = this; - this.options.filterView.choices.reset(this.options.filterView.choices.filter(function (item) { - return item.get('checked'); - })); - _.each(this.model.get('choices'), function (v, k) { - that.options.filterView.choices.add(new Backbone.Model({ id: k, text: v })); - }); - }, - - onShow () { - ChoiceFilters.DetailsChoiceFilterView.prototype.onShow.apply(this, arguments); - this.resetChoices(); - this.render(); - this.$('.navigator-filter-search input').focus(); - } - -}); - -const AjaxSelectFilterView = ChoiceFilters.ChoiceFilterView.extend({ - - initialize (options) { - ChoiceFilters.ChoiceFilterView.prototype.initialize.call(this, { - projectsView: (options && options.projectsView) ? options.projectsView : AjaxSelectDetailsFilterView - }); - }, - - isDefaultValue () { - return this.getSelected().length === 0; - }, - - renderInput () { - const value = this.model.get('value') || []; - const input = $('') - .prop('name', this.model.get('property')) - .prop('type', 'hidden') - .css('display', 'none') - .val(value.join()); - input.appendTo(this.$el); - }, - - restoreFromQuery (q) { - let param = _.findWhere(q, { key: this.model.get('property') }); - - if (this.model.get('choices')) { - _.each(this.model.get('choices'), function (v, k) { - if (k[0] === '!') { - const x = _.findWhere(q, { key: k.substr(1) }); - if (x == null) { - return; - } - if (!param) { - param = { value: k }; - } else { - param.value += ',' + k; - } - } - }); - } - - if (param && param.value) { - this.model.set('enabled', true); - this.restore(param.value, param); - } else { - this.clear(); - } - }, - - restore (value, param) { - const that = this; - if (_.isString(value)) { - value = value.split(','); - } - - if (this.choices && value.length > 0) { - this.model.set({ value, enabled: true }); - - const opposite = _.filter(value, function (item) { - return item[0] === '!'; - }); - opposite.forEach(function (item) { - that.choices.add(new Backbone.Model({ - id: item, - text: that.model.get('choices')[item], - checked: true - })); - }); - - value = _.reject(value, function (item) { - return item[0] === '!'; - }); - if (_.isArray(param.text) && param.text.length === value.length) { - this.restoreFromText(value, param.text); - } else { - this.restoreByRequests(value); - } - } else { - this.clear(); - } - }, - - restoreFromText (value, text) { - const that = this; - _.each(value, function (v, i) { - that.choices.add(new Backbone.Model({ - id: v, - text: text[i], - checked: true - })); - }); - this.onRestore(value); - }, - - restoreByRequests (value) { - const that = this; - const requests = _.map(value, function (v) { - return that.createRequest(v); - }); - - $.when.apply($, requests).done(function () { - that.onRestore(value); - }); - }, - - onRestore () { - this.projectsView.updateLists(); - this.renderBase(); - }, - - clear () { - this.model.unset('value'); - if (this.choices) { - this.choices.reset([]); - } - this.render(); - }, - - createRequest () { - } - -}); - -const ComponentFilterView = AjaxSelectFilterView.extend({ - - initialize () { - AjaxSelectFilterView.prototype.initialize.call(this, { - projectsView: AjaxSelectDetailsFilterView - }); - this.choices = new ComponentSuggestions(); - }, - - createRequest (v) { - const that = this; - return $ - .ajax({ - url: window.baseUrl + '/api/resources', - type: 'GET', - data: { resource: v } - }) - .done(function (r) { - that.selection.add(new Backbone.Model({ - id: r[0].key, - text: r[0].name - })); - }); - } - -}); - -const ProjectFilterView = AjaxSelectFilterView.extend({ - - initialize () { - BaseFilters.BaseFilterView.prototype.initialize.call(this, { - projectsView: AjaxSelectDetailsFilterView - }); - - this.choices = new ProjectSuggestions(); - }, - - createRequest (v) { - const that = this; - return $ - .ajax({ - url: window.baseUrl + '/api/resources', - type: 'GET', - data: { resource: v } - }) - .done(function (r) { - that.choices.add(new Backbone.Model({ - id: r[0].key, - text: r[0].name, - checked: true - })); - }); - } - -}); - -const AssigneeFilterView = AjaxSelectFilterView.extend({ - - initialize () { - BaseFilters.BaseFilterView.prototype.initialize.call(this, { - projectsView: AjaxSelectDetailsFilterView - }); - - this.choices = new UserSuggestions(); - }, - - createRequest (v) { - const that = this; - return $ - .ajax({ - url: window.baseUrl + '/api/users/search', - type: 'GET', - data: { q: v } - }) - .done(function (r) { - that.choices.add(new Backbone.Model({ - id: r.users[0].login, - text: r.users[0].name + ' (' + r.users[0].login + ')', - checked: true - })); - }); - } - -}); - -/* - * Export public classes - */ - -export default { - Suggestions, - AjaxSelectDetailsFilterView, - AjaxSelectFilterView, - ProjectFilterView, - ComponentFilterView, - AssigneeFilterView -}; - diff --git a/server/sonar-web/src/main/js/components/navigator/filters/base-filters.js b/server/sonar-web/src/main/js/components/navigator/filters/base-filters.js deleted file mode 100644 index ef9cb3c8315..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/filters/base-filters.js +++ /dev/null @@ -1,227 +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 'jquery'; -import _ from 'underscore'; -import Backbone from 'backbone'; -import Marionette from 'backbone.marionette'; -import Template from '../templates/base-filter.hbs'; -import DetailsTemplate from '../templates/base-details-filter.hbs'; - -const Filter = Backbone.Model.extend({ - - defaults: { - enabled: true, - optional: false, - multiple: true, - placeholder: '' - } - -}); - -const Filters = Backbone.Collection.extend({ - model: Filter -}); - -const DetailsFilterView = Marionette.ItemView.extend({ - template: DetailsTemplate, - className: 'navigator-filter-details', - - initialize () { - this.$el.on('click', function (e) { - e.stopPropagation(); - }); - this.$el.attr('id', 'filter-' + this.model.get('property')); - }, - - onShow () { - }, - - onHide () { - } -}); - -const BaseFilterView = Marionette.ItemView.extend({ - template: Template, - className: 'navigator-filter', - - events () { - return { - 'click': 'toggleDetails', - 'click .navigator-filter-disable': 'disable' - }; - }, - - modelEvents: { - 'change:enabled': 'focus', - 'change:value': 'renderBase', - - // for more criteria filter - 'change:filters': 'render' - }, - - initialize (options) { - Marionette.ItemView.prototype.initialize.apply(this, arguments); - - const DetailsView = (options && options.projectsView) || DetailsFilterView; - this.projectsView = new DetailsView({ - model: this.model, - filterView: this - }); - - this.model.view = this; - }, - - attachDetailsView () { - this.projectsView.$el.detach().appendTo($('body')); - }, - - render () { - this.renderBase(); - - this.attachDetailsView(); - this.projectsView.render(); - - this.$el.toggleClass( - 'navigator-filter-disabled', - !this.model.get('enabled')); - - this.$el.toggleClass( - 'navigator-filter-optional', - this.model.get('optional')); - }, - - renderBase () { - Marionette.ItemView.prototype.render.apply(this, arguments); - this.renderInput(); - - const title = this.model.get('name') + ': ' + this.renderValue(); - this.$el.prop('title', title); - this.$el.attr('data-property', this.model.get('property')); - }, - - renderInput () { - }, - - focus () { - this.render(); - }, - - toggleDetails (e) { - e.stopPropagation(); - this.options.filterBarView.selected = this.options.filterBarView.getEnabledFilters().index(this.$el); - if (this.$el.hasClass('active')) { - key.setScope('list'); - this.hideDetails(); - } else { - key.setScope('filters'); - this.showDetails(); - } - }, - - showDetails () { - this.registerShowedDetails(); - - const top = this.$el.offset().top + this.$el.outerHeight() - 1; - const left = this.$el.offset().left; - - this.projectsView.$el.css({ top, left }).addClass('active'); - this.$el.addClass('active'); - this.projectsView.onShow(); - }, - - registerShowedDetails () { - this.options.filterBarView.hideDetails(); - this.options.filterBarView.showedView = this; - }, - - hideDetails () { - this.projectsView.$el.removeClass('active'); - this.$el.removeClass('active'); - this.projectsView.onHide(); - }, - - isActive () { - return this.$el.is('.active'); - }, - - renderValue () { - return this.model.get('value') || 'unset'; - }, - - isDefaultValue () { - return true; - }, - - restoreFromQuery (q) { - const param = _.findWhere(q, { key: this.model.get('property') }); - if (param && param.value) { - this.model.set('enabled', true); - this.restore(param.value, param); - } else { - this.clear(); - } - }, - - restore (value) { - this.model.set({ value }, { silent: true }); - this.renderBase(); - }, - - clear () { - this.model.unset('value'); - }, - - disable (e) { - e.stopPropagation(); - this.hideDetails(); - this.options.filterBarView.hideDetails(); - this.model.set({ - enabled: false, - value: null - }); - }, - - formatValue () { - const q = {}; - if (this.model.has('property') && this.model.has('value') && this.model.get('value')) { - q[this.model.get('property')] = this.model.get('value'); - } - return q; - }, - - serializeData () { - return _.extend({}, this.model.toJSON(), { - value: this.renderValue(), - defaultValue: this.isDefaultValue() - }); - } - -}); - -/* - * Export public classes - */ - -export default { - Filter, - Filters, - BaseFilterView, - DetailsFilterView -}; diff --git a/server/sonar-web/src/main/js/components/navigator/filters/checkbox-filters.js b/server/sonar-web/src/main/js/components/navigator/filters/checkbox-filters.js deleted file mode 100644 index 78b2e1c7fcc..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/filters/checkbox-filters.js +++ /dev/null @@ -1,65 +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 'jquery'; -import BaseFilters from './base-filters'; -import Template from '../templates/checkbox-filter.hbs'; - -export default BaseFilters.BaseFilterView.extend({ - template: Template, - className: 'navigator-filter navigator-filter-inline', - - events () { - return { - 'click .navigator-filter-disable': 'disable' - }; - }, - - showDetails () { - }, - - renderInput () { - if (this.model.get('enabled')) { - $('') - .prop('name', this.model.get('property')) - .prop('type', 'checkbox') - .prop('value', 'true') - .prop('checked', true) - .css('display', 'none') - .appendTo(this.$el); - } - }, - - renderValue () { - return this.model.get('value'); - }, - - isDefaultValue () { - return false; - }, - - restore (value) { - this.model.set({ - value, - enabled: true - }); - } - -}); - diff --git a/server/sonar-web/src/main/js/components/navigator/filters/choice-filters.js b/server/sonar-web/src/main/js/components/navigator/filters/choice-filters.js deleted file mode 100644 index 7f1b82d9b3a..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/filters/choice-filters.js +++ /dev/null @@ -1,383 +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 'jquery'; -import _ from 'underscore'; -import Backbone from 'backbone'; -import BaseFilters from './base-filters'; -import Template from '../templates/choice-filter.hbs'; -import ItemTemplate from '../templates/choice-filter-item.hbs'; -import { translate } from '../../../helpers/l10n'; - -const DetailsChoiceFilterView = BaseFilters.DetailsFilterView.extend({ - template: Template, - itemTemplate: ItemTemplate, - - events () { - return { - 'click label': 'onCheck' - }; - }, - - render () { - BaseFilters.DetailsFilterView.prototype.render.apply(this, arguments); - this.updateLists(); - }, - - renderList (collection, selector) { - const that = this; - const container = this.$(selector); - - container.empty().toggleClass('hidden', collection.length === 0); - collection.each(function (item) { - container.append( - that.itemTemplate(_.extend(item.toJSON(), { - multiple: that.model.get('multiple') && item.get('id')[0] !== '!' - })) - ); - }); - }, - - updateLists () { - const choices = new Backbone.Collection(this.options.filterView.choices.reject(function (item) { - return item.get('id')[0] === '!'; - })); - const opposite = new Backbone.Collection(this.options.filterView.choices.filter(function (item) { - return item.get('id')[0] === '!'; - })); - - this.renderList(choices, '.choices'); - this.renderList(opposite, '.opposite'); - - const current = this.currentChoice || 0; - this.updateCurrent(current); - }, - - onCheck (e) { - const checkbox = $(e.currentTarget); - const id = checkbox.data('id'); - const checked = checkbox.find('.icon-checkbox-checked').length > 0; - - if (this.model.get('multiple')) { - if (checkbox.closest('.opposite').length > 0) { - this.options.filterView.choices.each(function (item) { - item.set('checked', false); - }); - } else { - this.options.filterView.choices.filter(function (item) { - return item.get('id')[0] === '!'; - }).forEach(function (item) { - item.set('checked', false); - }); - } - } else { - this.options.filterView.choices.each(function (item) { - item.set('checked', false); - }); - } - - this.options.filterView.choices.get(id).set('checked', !checked); - this.updateValue(); - this.updateLists(); - }, - - updateValue () { - this.model.set('value', this.options.filterView.getSelected().map(function (m) { - return m.get('id'); - })); - }, - - updateCurrent (index) { - this.currentChoice = index; - this.$('label').removeClass('current') - .eq(this.currentChoice).addClass('current'); - }, - - onShow () { - this.bindedOnKeyDown = _.bind(this.onKeyDown, this); - $('body').on('keydown', this.bindedOnKeyDown); - }, - - onHide () { - $('body').off('keydown', this.bindedOnKeyDown); - }, - - onKeyDown (e) { - switch (e.keyCode) { - case 38: - e.preventDefault(); - this.selectPrevChoice(); - break; - case 40: - e.preventDefault(); - this.selectNextChoice(); - break; - case 13: - e.preventDefault(); - this.selectCurrent(); - break; - default: - - // Not a functional key - then skip - break; - } - }, - - selectNextChoice () { - if (this.$('label').length > this.currentChoice + 1) { - this.updateCurrent(this.currentChoice + 1); - this.scrollNext(); - } - }, - - scrollNext () { - const currentLabel = this.$('label').eq(this.currentChoice); - if (currentLabel.length > 0) { - const list = currentLabel.closest('ul'); - const labelPos = currentLabel.offset().top - list.offset().top + list.scrollTop(); - const deltaScroll = labelPos - list.height() + currentLabel.outerHeight(); - - if (deltaScroll > 0) { - list.scrollTop(deltaScroll); - } - } - }, - - selectPrevChoice () { - if (this.currentChoice > 0) { - this.updateCurrent(this.currentChoice - 1); - this.scrollPrev(); - } - }, - - scrollPrev () { - const currentLabel = this.$('label').eq(this.currentChoice); - if (currentLabel.length > 0) { - const list = currentLabel.closest('ul'); - const labelPos = currentLabel.offset().top - list.offset().top; - - if (labelPos < 0) { - list.scrollTop(list.scrollTop() + labelPos); - } - } - }, - - selectCurrent () { - const cb = this.$('label').eq(this.currentChoice); - cb.click(); - }, - - serializeData () { - return _.extend({}, this.model.toJSON(), { - choices: new Backbone.Collection(this.options.filterView.choices.reject(function (item) { - return item.get('id')[0] === '!'; - })).toJSON(), - opposite: new Backbone.Collection(this.options.filterView.choices.filter(function (item) { - return item.get('id')[0] === '!'; - })).toJSON() - }); - } - -}); - -const ChoiceFilterView = BaseFilters.BaseFilterView.extend({ - - initialize (options) { - BaseFilters.BaseFilterView.prototype.initialize.call(this, { - projectsView: (options && options.projectsView) ? options.projectsView : DetailsChoiceFilterView - }); - - let index = 0; - const icons = this.model.get('choiceIcons'); - - this.choices = new Backbone.Collection( - _.map(this.model.get('choices'), function (value, key) { - const model = new Backbone.Model({ - id: key, - text: value, - checked: false, - index: index++ - }); - - if (icons && icons[key]) { - model.set('icon', icons[key]); - } - - return model; - }), { comparator: 'index' } - ); - }, - - getSelected () { - return this.choices.filter(function (m) { - return m.get('checked'); - }); - }, - - renderInput () { - const input = $('') - .prop('name', that.model.get('property') + '_' + key) - .prop('type', 'hidden') - .css('display', 'none') - .val(v) - .appendTo(that.$el); - }); - } - }, - - isDefaultValue () { - const value = this.model.get('value'); - if (!_.isObject(value)) { - return true; - } - return !(value.metric && value.op && (value.val != null)); - }, - - restoreFromQuery (q) { - const that = this; - const value = {}; - _.each(['metric', 'period', 'op', 'val'], function (p) { - const property = that.model.get('property') + '_' + p; - const pValue = _.findWhere(q, { key: property }); - - if (pValue && pValue.value) { - value[p] = pValue.value; - } - }); - - if (value.metric && value.op && (value.val != null)) { - this.model.set({ - value, - enabled: true - }); - } - } - -}); - diff --git a/server/sonar-web/src/main/js/components/navigator/filters/more-criteria-filters.js b/server/sonar-web/src/main/js/components/navigator/filters/more-criteria-filters.js deleted file mode 100644 index acf1f0b812a..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/filters/more-criteria-filters.js +++ /dev/null @@ -1,107 +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 'jquery'; -import _ from 'underscore'; -import ChoiceFilters from './choice-filters'; -import Template from '../templates/more-criteria-filter.hbs'; -import DetailsTemplate from '../templates/more-criteria-details-filter.hbs'; - -const DetailsMoreCriteriaFilterView = ChoiceFilters.DetailsChoiceFilterView.extend({ - template: DetailsTemplate, - - events: { - 'click label[data-id]:not(.inactive)': 'enableFilter' - }, - - enableById (id) { - this.model.view.options.filterBarView.enableFilter(id); - this.model.view.hideDetails(); - }, - - enableByProperty (property) { - const filter = _.find(this.model.get('filters'), function (f) { - return f.get('property') === property; - }); - if (filter) { - this.enableById(filter.cid); - } - }, - - enableFilter (e) { - const id = $(e.target).data('id'); - this.enableById(id); - this.updateCurrent(0); - }, - - selectCurrent () { - this.$('label').eq(this.currentChoice).click(); - }, - - serializeData () { - const filters = this.model.get('filters').map(function (filter) { - return _.extend(filter.toJSON(), { id: filter.cid }); - }); - const getName = function (filter) { - return filter.name; - }; - const uniqueFilters = _.unique(filters, getName); - const sortedFilters = _.sortBy(uniqueFilters, getName); - return _.extend(this.model.toJSON(), { filters: sortedFilters }); - } - -}); - -const MoreCriteriaFilterView = ChoiceFilters.ChoiceFilterView.extend({ - template: Template, - className: 'navigator-filter navigator-filter-more-criteria', - - initialize () { - ChoiceFilters.ChoiceFilterView.prototype.initialize.call(this, { - projectsView: DetailsMoreCriteriaFilterView - }); - }, - - renderValue () { - return ''; - }, - - renderInput () { - }, - - renderBase () { - ChoiceFilters.ChoiceFilterView.prototype.renderBase.call(this); - this.$el.prop('title', ''); - }, - - isDefaultValue () { - return false; - } - -}); - -/* - * Export public classes - */ - -export default { - DetailsMoreCriteriaFilterView, - MoreCriteriaFilterView -}; - diff --git a/server/sonar-web/src/main/js/components/navigator/filters/range-filters.js b/server/sonar-web/src/main/js/components/navigator/filters/range-filters.js deleted file mode 100644 index 3ae434f9e5e..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/filters/range-filters.js +++ /dev/null @@ -1,208 +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 'jquery'; -import _ from 'underscore'; -import BaseFilters from './base-filters'; -import Template from '../templates/range-filter.hbs'; -import { translate } from '../../../helpers/l10n'; - -const DetailsRangeFilterView = BaseFilters.DetailsFilterView.extend({ - template: Template, - - events: { - 'change input': 'change' - }, - - change () { - const value = {}; - const valueFrom = this.$('input').eq(0).val(); - const valueTo = this.$('input').eq(1).val(); - - if (valueFrom.length > 0) { - value[this.model.get('propertyFrom')] = valueFrom; - } - - if (valueTo.length > 0) { - value[this.model.get('propertyTo')] = valueTo; - } - - this.model.set('value', value); - }, - - populateInputs () { - const value = this.model.get('value'); - const propertyFrom = this.model.get('propertyFrom'); - const propertyTo = this.model.get('propertyTo'); - const valueFrom = _.isObject(value) && value[propertyFrom]; - const valueTo = _.isObject(value) && value[propertyTo]; - - this.$('input').eq(0).val(valueFrom || ''); - this.$('input').eq(1).val(valueTo || ''); - }, - - onShow () { - this.$(':input:first').focus(); - } - -}); - -const RangeFilterView = BaseFilters.BaseFilterView.extend({ - - initialize () { - BaseFilters.BaseFilterView.prototype.initialize.call(this, { - projectsView: DetailsRangeFilterView - }); - }, - - renderValue () { - if (!this.isDefaultValue()) { - const value = _.values(this.model.get('value')); - return value.join(' — '); - } else { - return translate('any'); - } - }, - - renderInput () { - const value = this.model.get('value'); - const propertyFrom = this.model.get('propertyFrom'); - const propertyTo = this.model.get('propertyTo'); - const valueFrom = _.isObject(value) && value[propertyFrom]; - const valueTo = _.isObject(value) && value[propertyTo]; - - $('') - .prop('name', propertyFrom) - .prop('type', 'hidden') - .css('display', 'none') - .val(valueFrom || '') - .appendTo(this.$el); - - $('') - .prop('name', propertyTo) - .prop('type', 'hidden') - .css('display', 'none') - .val(valueTo || '') - .appendTo(this.$el); - }, - - isDefaultValue () { - const value = this.model.get('value'); - const propertyFrom = this.model.get('propertyFrom'); - const propertyTo = this.model.get('propertyTo'); - const valueFrom = _.isObject(value) && value[propertyFrom]; - const valueTo = _.isObject(value) && value[propertyTo]; - - return !valueFrom && !valueTo; - }, - - restoreFromQuery (q) { - const paramFrom = _.findWhere(q, { key: this.model.get('propertyFrom') }); - const paramTo = _.findWhere(q, { key: this.model.get('propertyTo') }); - const value = {}; - - if ((paramFrom && paramFrom.value) || (paramTo && paramTo.value)) { - if (paramFrom && paramFrom.value) { - value[this.model.get('propertyFrom')] = paramFrom.value; - } - - if (paramTo && paramTo.value) { - value[this.model.get('propertyTo')] = paramTo.value; - } - - this.model.set({ - value, - enabled: true - }); - - this.projectsView.populateInputs(); - } - }, - - restore (value) { - if (this.choices && this.selection && value.length > 0) { - const that = this; - this.choices.add(this.selection.models); - this.selection.reset([]); - - _.each(value, function (v) { - const cModel = that.choices.findWhere({ id: v }); - - if (cModel) { - that.selection.add(cModel); - that.choices.remove(cModel); - } - }); - - this.projectsView.updateLists(); - - this.model.set({ - value, - enabled: true - }); - } - }, - - formatValue () { - return this.model.get('value'); - }, - - clear () { - this.model.unset('value'); - this.projectsView.render(); - } - -}); - -const DateRangeFilterView = RangeFilterView.extend({ - - render () { - RangeFilterView.prototype.render.apply(this, arguments); - this.projectsView.$('input') - .prop('placeholder', '1970-01-31') - .datepicker({ - dateFormat: 'yy-mm-dd', - changeMonth: true, - changeYear: true - }) - .on('change', function () { - $(this).datepicker('setDate', $(this).val()); - }); - }, - - renderValue () { - if (!this.isDefaultValue()) { - const value = _.values(this.model.get('value')); - return value.join(' — '); - } else { - return translate('anytime'); - } - } - -}); - -/* - * Export public classes - */ - -export default { - RangeFilterView, - DateRangeFilterView -}; - diff --git a/server/sonar-web/src/main/js/components/navigator/filters/string-filters.js b/server/sonar-web/src/main/js/components/navigator/filters/string-filters.js deleted file mode 100644 index b1ff255083e..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/filters/string-filters.js +++ /dev/null @@ -1,87 +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 'jquery'; -import _ from 'underscore'; -import BaseFilters from './base-filters'; -import Template from '../templates/string-filter.hbs'; - -const DetailsStringFilterView = BaseFilters.DetailsFilterView.extend({ - template: Template, - - events: { - 'change input': 'change' - }, - - change (e) { - this.model.set('value', $(e.target).val()); - }, - - onShow () { - BaseFilters.DetailsFilterView.prototype.onShow.apply(this, arguments); - this.$(':input').focus(); - }, - - serializeData () { - return _.extend({}, this.model.toJSON(), { - value: this.model.get('value') || '' - }); - } - -}); - -export default BaseFilters.BaseFilterView.extend({ - - initialize () { - BaseFilters.BaseFilterView.prototype.initialize.call(this, { - projectsView: DetailsStringFilterView - }); - }, - - renderValue () { - return this.isDefaultValue() ? '—' : this.model.get('value'); - }, - - renderInput () { - $('') - .prop('name', this.model.get('property')) - .prop('type', 'hidden') - .css('display', 'none') - .val(this.model.get('value') || '') - .appendTo(this.$el); - }, - - isDefaultValue () { - return !this.model.get('value'); - }, - - restore (value) { - this.model.set({ - value, - enabled: true - }); - }, - - clear () { - this.model.unset('value'); - this.projectsView.render(); - } - -}); - diff --git a/server/sonar-web/src/main/js/components/store/globalMessages.js b/server/sonar-web/src/main/js/components/store/globalMessages.js index 038c74f0132..e813f6f59b8 100644 --- a/server/sonar-web/src/main/js/components/store/globalMessages.js +++ b/server/sonar-web/src/main/js/components/store/globalMessages.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import uniqueId from 'lodash/uniqueId'; +import { actions } from '../../app/store/appState/duck'; export const ERROR = 'ERROR'; export const SUCCESS = 'SUCCESS'; @@ -54,6 +55,24 @@ const globalMessages = (state = [], action = {}) => { }]; } + if (action.type === actions.REQUIRE_AUTHENTICATION) { + // FIXME l10n + return [{ + id: uniqueId('global-message-'), + message: 'Authentication required to see this page.', + level: ERROR + }]; + } + + if (action.type === actions.REQUIRE_AUTHORIZATION) { + // FIXME l10n + return [{ + id: uniqueId('global-message-'), + message: 'You are not authorized to access this page. Please log in with more privileges and try again.', + level: ERROR + }]; + } + if (action.type === CLOSE_ALL_GLOBAL_MESSAGES) { return []; } diff --git a/server/sonar-web/src/main/js/components/ui/Avatar.js b/server/sonar-web/src/main/js/components/ui/Avatar.js index 53e4b90af11..8e96bb380ae 100644 --- a/server/sonar-web/src/main/js/components/ui/Avatar.js +++ b/server/sonar-web/src/main/js/components/ui/Avatar.js @@ -18,24 +18,27 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { connect } from 'react-redux'; import md5 from 'blueimp-md5'; import classNames from 'classnames'; +import { getSettingValue } from '../../app/store/rootReducer'; -export default class Avatar extends React.Component { +class Avatar extends React.Component { static propTypes = { + enableGravatar: React.PropTypes.bool.isRequired, + gravatarServerUrl: React.PropTypes.string.isRequired, email: React.PropTypes.string, size: React.PropTypes.number.isRequired, className: React.PropTypes.string }; render () { - const shouldShowAvatar = window.SS && window.SS.lf && window.SS.lf.enableGravatar; - if (!shouldShowAvatar) { + if (!this.props.enableGravatar) { return null; } const emailHash = md5.md5((this.props.email || '').toLowerCase()).trim(); - const url = ('' + window.SS.lf.gravatarServerUrl) + const url = this.props.gravatarServerUrl .replace('{EMAIL_MD5}', emailHash) .replace('{SIZE}', this.props.size * 2); @@ -50,3 +53,12 @@ export default class Avatar extends React.Component { ); } } + +const mapStateToProps = state => ({ + enableGravatar: (getSettingValue(state, 'sonar.lf.enableGravatar') || {}).value === 'true', + gravatarServerUrl: (getSettingValue(state, 'sonar.lf.gravatarServerUrl') || {}).value +}); + +export default connect(mapStateToProps)(Avatar); + +export const unconnectedAvatar = Avatar; diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js b/server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js index 1a5239d0727..a6cd4cf6115 100644 --- a/server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js +++ b/server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js @@ -19,23 +19,14 @@ */ import { shallow } from 'enzyme'; import React from 'react'; -import Avatar from '../Avatar'; +import { unconnectedAvatar as Avatar } from '../Avatar'; -beforeEach(() => { - window.SS = { - lf: { - enableGravatar: true, - gravatarServerUrl: 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}' - } - }; -}); - -afterEach(() => { - window.SS = undefined; -}); +const gravatarServerUrl = 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}'; it('should render', () => { - const avatar = shallow(); + const avatar = shallow( + + ); expect(avatar.is('img')).toBe(true); expect(avatar.prop('width')).toBe(20); expect(avatar.prop('height')).toBe(20); @@ -44,7 +35,8 @@ it('should render', () => { }); it('should not render', () => { - window.SS.lf.enableGravatar = false; - const avatar = shallow(); + const avatar = shallow( + + ); expect(avatar.is('img')).toBe(false); }); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/measures-test.js b/server/sonar-web/src/main/js/helpers/__tests__/measures-test.js index 19aa065750d..438fd223f90 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/measures-test.js +++ b/server/sonar-web/src/main/js/helpers/__tests__/measures-test.js @@ -35,7 +35,6 @@ beforeEach(function () { 'metric.level.WARN': 'Warning', 'metric.level.OK': 'Ok' }); - window.SS = { hoursInDay: HOURS_IN_DAY }; }); describe('#formatMeasure()', function () { diff --git a/server/sonar-web/src/main/js/helpers/cookies.js b/server/sonar-web/src/main/js/helpers/cookies.js index b9b8062df7a..5ec17e2ef32 100644 --- a/server/sonar-web/src/main/js/helpers/cookies.js +++ b/server/sonar-web/src/main/js/helpers/cookies.js @@ -17,9 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +// @flow let cookies; -export function getCookie (name) { +export function getCookie (name: string) { if (cookies) { return cookies[name]; } diff --git a/server/sonar-web/src/main/js/helpers/csv.js b/server/sonar-web/src/main/js/helpers/csv.js index 57d372a6d2b..89f883ac93b 100644 --- a/server/sonar-web/src/main/js/helpers/csv.js +++ b/server/sonar-web/src/main/js/helpers/csv.js @@ -17,7 +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. */ -export function csvEscape (value) { +// @flow +export function csvEscape (value: string): string { const escaped = value.replace(/"/g, '\\"'); return `"${escaped}"`; } diff --git a/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js b/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js index 1e913674f90..c790508b1c0 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js @@ -20,10 +20,18 @@ import md5 from 'blueimp-md5'; import Handlebars from 'handlebars/runtime'; +function gravatarServer () { + const getStore = require('../../app/utils/getStore').default; + const { getSettingValue } = require('../../app/store/rootReducer'); + + const store = getStore(); + return (getSettingValue(store.getState(), 'sonar.lf.gravatarServerUrl') || {}).value; +} + module.exports = function (email, size) { // double the size for high pixel density screens const emailHash = md5.md5((email || '').trim()); - const url = ('' + window.SS.lf.gravatarServerUrl) + const url = gravatarServer() .replace('{EMAIL_MD5}', emailHash) .replace('{SIZE}', size * 2); return new Handlebars.default.SafeString( diff --git a/server/sonar-web/src/main/js/helpers/handlebars/ifShowAvatars.js b/server/sonar-web/src/main/js/helpers/handlebars/ifShowAvatars.js index 0290074792f..87f038f1fa6 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/ifShowAvatars.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/ifShowAvatars.js @@ -17,7 +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. */ +function enableGravatar () { + const getStore = require('../../app/utils/getStore').default; + const { getSettingValue } = require('../../app/store/rootReducer'); + + const store = getStore(); + return (getSettingValue(store.getState(), 'sonar.lf.enableGravatar') || {}).value === 'true'; +} + module.exports = function (options) { - const cond = window.SS && window.SS.lf && window.SS.lf.enableGravatar; - return cond ? options.fn(this) : options.inverse(this); + return enableGravatar() ? options.fn(this) : options.inverse(this); }; diff --git a/server/sonar-web/src/main/js/helpers/l10n.js b/server/sonar-web/src/main/js/helpers/l10n.js index 6b8f85ef4fc..d77ff416682 100644 --- a/server/sonar-web/src/main/js/helpers/l10n.js +++ b/server/sonar-web/src/main/js/helpers/l10n.js @@ -17,7 +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. */ - /* @flow */ +/* @flow */ import moment from 'moment'; import { request } from './request'; @@ -63,7 +63,9 @@ function makeRequest (params) { case 401: window.location = window.baseUrl + '/sessions/new?return_to=' + encodeURIComponent(window.location.pathname + window.location.search + window.location.hash); - return {}; + // return unresolved promise to stop the promise chain + // anyway the page will be reloaded + return new Promise(() => {}); default: throw new Error('Unexpected status code: ' + response.status); } diff --git a/server/sonar-web/src/main/js/helpers/measures.js b/server/sonar-web/src/main/js/helpers/measures.js index 14cbd630c3a..6a7cd4a2d0e 100644 --- a/server/sonar-web/src/main/js/helpers/measures.js +++ b/server/sonar-web/src/main/js/helpers/measures.js @@ -302,11 +302,21 @@ function formatDurationShort (isNegative, days, hours, minutes) { return translateWithParameters('work_duration.x_minutes', formattedMinutes); } +function getHoursInDay () { + // workaround cyclic dependencies + const getStore = require('../app/utils/getStore').default; + const { getSettingValue } = require('../app/store/rootReducer'); + + const store = getStore(); + const settingValue = getSettingValue(store.getState(), 'sonar.technicalDebt.hoursInDay'); + return settingValue ? settingValue.value : 8; +} + function durationFormatter (value) { if (value === 0 || value === '0') { return '0'; } - const hoursInDay = window.SS.hoursInDay; + const hoursInDay = getHoursInDay(); const isNegative = value < 0; const absValue = Math.abs(value); const days = Math.floor(absValue / hoursInDay / 60); @@ -321,7 +331,7 @@ function shortDurationFormatter (value) { if (value === 0 || value === '0') { return '0'; } - const hoursInDay = window.SS.hoursInDay; + const hoursInDay = getHoursInDay(); const isNegative = value < 0; const absValue = Math.abs(value); const days = absValue / hoursInDay / 60; @@ -347,13 +357,23 @@ function shortDurationVariationFormatter (value) { return formatted[0] !== '-' ? '+' + formatted : formatted; } +function getRatingGrid () { + // workaround cyclic dependencies + const getStore = require('../app/utils/getStore').default; + const { getSettingValue } = require('../app/store/rootReducer'); + + const store = getStore(); + const settingValue = getSettingValue(store.getState(), 'sonar.technicalDebt.ratingGrid'); + return settingValue ? settingValue.value : ''; +} + let maintainabilityRatingGrid; function getMaintainabilityRatingGrid () { if (maintainabilityRatingGrid) { return maintainabilityRatingGrid; } - const str = window.SS['sonar.technicalDebt.ratingGrid']; + const str = getRatingGrid(); const numbers = str.split(',') .map(s => parseFloat(s)) .filter(n => !isNaN(n)); diff --git a/server/sonar-web/src/main/js/helpers/request.js b/server/sonar-web/src/main/js/helpers/request.js index 349392af366..b58d1dda053 100644 --- a/server/sonar-web/src/main/js/helpers/request.js +++ b/server/sonar-web/src/main/js/helpers/request.js @@ -17,14 +17,20 @@ * 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 { stringify } from 'querystring'; import { getCookie } from './cookies'; -export function getCSRFTokenName () { +type Response = { + json: () => Promise, + status: number +}; + +export function getCSRFTokenName (): string { return 'X-XSRF-TOKEN'; } -export function getCSRFTokenValue () { +export function getCSRFTokenValue (): string { const cookieName = 'XSRF-TOKEN'; const cookieValue = getCookie(cookieName); if (!cookieValue) { @@ -37,7 +43,7 @@ export function getCSRFTokenValue () { * Return an object containing a special http request header used to prevent CSRF attacks. * @returns {Object} */ -export function getCSRFToken () { +export function getCSRFToken (): Object { // Fetch API in Edge doesn't work with empty header, // so we ensure non-empty value const value = getCSRFTokenValue(); @@ -47,7 +53,10 @@ export function getCSRFToken () { /** * Default options for any request */ -const DEFAULT_OPTIONS = { +const DEFAULT_OPTIONS: { + credentials: string, + method: string +} = { method: 'GET', credentials: 'same-origin' }; @@ -55,7 +64,9 @@ const DEFAULT_OPTIONS = { /** * Default request headers */ -const DEFAULT_HEADERS = { +const DEFAULT_HEADERS: { + 'Accept': string +} = { 'Accept': 'application/json' }; @@ -63,14 +74,21 @@ const DEFAULT_HEADERS = { * Request */ class Request { - constructor (url) { + url: string; + options: { + method?: string + }; + headers: Object; + data: ?Object; + + constructor (url: string): void { this.url = url; this.options = {}; this.headers = {}; } submit () { - let url = this.url; + let url: string = this.url; const options = { ...DEFAULT_OPTIONS, ...this.options }; const customHeaders = {}; @@ -82,6 +100,7 @@ class Request { url += '?' + stringify(this.data); } else { customHeaders['Content-Type'] = 'application/x-www-form-urlencoded'; + // $FlowFixMe complains that `data` is nullable options.body = stringify(this.data); } } @@ -96,17 +115,17 @@ class Request { return window.fetch(window.baseUrl + url, options); } - setMethod (method) { + setMethod (method: string): Request { this.options.method = method; return this; } - setData (data) { + setData (data?: Object): Request { this.data = data; return this; } - setHeader (name, value) { + setHeader (name: string, value: string): Request { this.headers[name] = value; return this; } @@ -117,7 +136,7 @@ class Request { * @param {string} url * @returns {Request} */ -export function request (url) { +export function request (url: string): Request { return new Request(url); } @@ -126,15 +145,17 @@ export function request (url) { * @param response * @returns {*} */ -export function checkStatus (response) { +export function checkStatus (response: Response): Promise { if (response.status === 401) { - window.location = window.baseUrl + '/sessions/new?return_to=' + - encodeURIComponent(window.location.pathname + window.location.search + window.location.hash); - return {}; + // workaround cyclic dependencies + const handleRequiredAuthentication = require('../app/utils/handleRequiredAuthentication').default; + handleRequiredAuthentication(); + return Promise.reject(); } else if (response.status >= 200 && response.status < 300) { - return response; + return Promise.resolve(response); } else { const error = new Error(response.status); + // $FlowFixMe complains that `response` is not found error.response = response; throw error; } @@ -145,7 +166,7 @@ export function checkStatus (response) { * @param response * @returns {object} */ -export function parseJSON (response) { +export function parseJSON (response: Response): Promise { return response.json(); } @@ -154,7 +175,7 @@ export function parseJSON (response) { * @param url * @param data */ -export function getJSON (url, data) { +export function getJSON (url: string, data?: Object): Promise { return request(url) .setData(data) .submit() @@ -167,7 +188,7 @@ export function getJSON (url, data) { * @param url * @param data */ -export function postJSON (url, data) { +export function postJSON (url: string, data?: Object): Promise { return request(url) .setMethod('POST') .setData(data) @@ -181,7 +202,7 @@ export function postJSON (url, data) { * @param url * @param data */ -export function post (url, data) { +export function post (url: string, data?: Object): Promise { return request(url) .setMethod('POST') .setData(data) @@ -194,7 +215,7 @@ export function post (url, data) { * @param url * @param data */ -export function requestDelete (url, data) { +export function requestDelete (url: string, data?: Object): Promise { return request(url) .setMethod('DELETE') .setData(data) @@ -207,6 +228,6 @@ export function requestDelete (url, data) { * @param response * @returns {Promise} */ -export function delay (response) { +export function delay (response: any): Promise { return new Promise(resolve => setTimeout(() => resolve(response), 1200)); } diff --git a/server/sonar-web/src/main/js/helpers/handlebars/ifCanUseFilter.js b/server/sonar-web/src/main/js/helpers/users.js similarity index 83% rename from server/sonar-web/src/main/js/helpers/handlebars/ifCanUseFilter.js rename to server/sonar-web/src/main/js/helpers/users.js index ee8ed1691ad..2c116694d52 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/ifCanUseFilter.js +++ b/server/sonar-web/src/main/js/helpers/users.js @@ -17,7 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -module.exports = function (query, options) { - const cond = window.SS.user || query.indexOf('__me__') === -1; - return cond ? options.fn(this) : options.inverse(this); +// @flow +type User = { + permissions: { + global: Array + } }; + +export const isUserAdmin = (user: User): boolean => ( + user.permissions.global.includes('admin') +); diff --git a/server/sonar-web/src/main/js/libs/sonar.js b/server/sonar-web/src/main/js/libs/sonar.js deleted file mode 100644 index 366925e71fd..00000000000 --- a/server/sonar-web/src/main/js/libs/sonar.js +++ /dev/null @@ -1,50 +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. - */ -require('script!./third-party/jquery-ui.js'); -require('script!./third-party/select2.js'); -require('script!./third-party/keymaster.js'); -require('script!./third-party/bootstrap/tooltip.js'); -require('script!./third-party/bootstrap/dropdown.js'); -require('script!./select2-jquery-ui-fix.js'); -require('script!./inputs.js'); -require('script!./jquery-isolated-scroll.js'); -require('script!./application.js'); -var request = require('../helpers/request'); - -window.$j = jQuery.noConflict(); - -jQuery(function () { - jQuery('.open-modal').modal(); -}); - -jQuery.ajaxSetup({ - beforeSend: function (jqXHR) { - jqXHR.setRequestHeader(request.getCSRFTokenName(), request.getCSRFTokenValue()); - }, - statusCode: { - 401: function () { - window.location = window.baseUrl + '/sessions/new?return_to=' + - encodeURIComponent(window.location.pathname + window.location.search + window.location.hash); - } - } -}); - -window.sonarqube = {}; -window.sonarqube.el = '#content'; diff --git a/server/sonar-web/src/main/less/components/page.less b/server/sonar-web/src/main/less/components/page.less index d6e25d68ac7..370464a0d77 100644 --- a/server/sonar-web/src/main/less/components/page.less +++ b/server/sonar-web/src/main/less/components/page.less @@ -21,7 +21,7 @@ @import (reference) "../mixins"; @import (reference) "../init/links"; -body { +.global-container { display: flex; flex-direction: column; height: 100%; -- 2.39.5