summaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorlukasz-jarocki-sonarsource <77498856+lukasz-jarocki-sonarsource@users.noreply.github.com>2021-02-26 09:29:39 +0100
committersonartech <sonartech@sonarsource.com>2021-02-26 20:07:39 +0000
commit9cb17b6dbce261af578b7c5fe430fa340d4ff1ad (patch)
treedecb117810be86812b2849f8adb388de5a97919d /server
parent26ca7559fab8b6a0379468a0a5b5f7d83c1baaa9 (diff)
downloadsonarqube-9cb17b6dbce261af578b7c5fe430fa340d4ff1ad.tar.gz
sonarqube-9cb17b6dbce261af578b7c5fe430fa340d4ff1ad.zip
Revert SONAR-14478, SONAR-14462, SONAR-14461
* Revert "SONAR-14478 - Main Branch Documentation" This reverts commit 59eae7cf3f2e611e162a4e0122ae5846b10a45b1. * Revert "SONAR-14462 Do not display the branch name until the main branch is analyzed for the first time" This reverts commit 20f7319c06affdae62d39d1bad002f16504465a2. * Revert "SONAR-14461 main branch detection" This reverts commit c04baa1e8e3b492953d66a6bc4111c01f3ee3069. * Revert "SONAR-14461 Remove hardcoded usage of 'master'" This reverts commit 32eefaf2d36af375af280cc3ba664fd71e0f6afd. * Revert "SONAR-14461 save the default main branch when needed" This reverts commit 879a4be2afc570b2248fb4d639f42f913215805b.
Diffstat (limited to 'server')
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java1
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStep.java79
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStepTest.java136
-rw-r--r--server/sonar-docs/src/pages/analysis/branch-pr-analysis-overview.md10
-rw-r--r--server/sonar-docs/src/pages/branches/overview.md28
-rw-r--r--server/sonar-web/src/main/js/api/news.ts139
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx89
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx135
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap97
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx5
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx19
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap3
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx93
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx134
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx67
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx119
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap82
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap269
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/notifications.css157
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap181
26 files changed, 1434 insertions, 487 deletions
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
index f04fe03a2a9..bfca57e6c57 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
@@ -107,7 +107,6 @@ public class ReportComputationSteps extends AbstractComputationSteps {
PurgeDatastoresStep.class,
IndexAnalysisStep.class,
UpdateNeedIssueSyncStep.class,
- UpdateMainBranchStep.class,
// notifications are sent at the end, so that webapp displays up-to-date information
SendIssueNotificationsStep.class,
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStep.java
deleted file mode 100644
index 1d70c9b7389..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStep.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info 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.
- */
-package org.sonar.ce.task.projectanalysis.step;
-
-import java.util.Optional;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
-import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
-import org.sonar.ce.task.projectanalysis.batch.BatchReportReader;
-import org.sonar.ce.task.step.ComputationStep;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.project.ProjectDto;
-
-public class UpdateMainBranchStep implements ComputationStep {
-
- private static final Logger LOGGER = Loggers.get(UpdateMainBranchStep.class);
-
- private final BatchReportReader batchReportReader;
- private final DbClient dbClient;
- private final AnalysisMetadataHolder analysisMetadataHolder;
-
- public UpdateMainBranchStep(BatchReportReader batchReportReader, DbClient dbClient, AnalysisMetadataHolder analysisMetadataHolder) {
- this.batchReportReader = batchReportReader;
- this.dbClient = dbClient;
- this.analysisMetadataHolder = analysisMetadataHolder;
- }
-
- @Override
- public void execute(Context context) {
-
- if (!analysisMetadataHolder.isFirstAnalysis()) {
- return;
- }
-
- String gitDefaultMainBranch = batchReportReader.readMetadata().getGitDefaultMainBranch();
- if (gitDefaultMainBranch.isEmpty()) {
- LOGGER.debug("GIT default main branch detected is empty");
- return;
- }
- LOGGER.debug(String.format("GIT default main branch detected is [%s]", gitDefaultMainBranch));
- updateProjectDefaultMainBranch(gitDefaultMainBranch);
- }
-
- private void updateProjectDefaultMainBranch(String gitDefaultMainBranch) {
- try (DbSession dbSession = dbClient.openSession(false)) {
- String projectKey = analysisMetadataHolder.getProject().getKey();
- Optional<ProjectDto> projectDto = dbClient.projectDao().selectProjectByKey(dbSession, projectKey);
- if (!projectDto.isPresent()) {
- throw new IllegalStateException(String.format("root component key [%s] is not a project", projectKey));
- }
- LOGGER.info(String.format("updating project [%s] default main branch to [%s]", projectKey, gitDefaultMainBranch));
- dbClient.branchDao().updateMainBranchName(dbSession, projectDto.get().getUuid(), gitDefaultMainBranch);
- dbSession.commit();
- }
- }
-
- @Override
- public String getDescription() {
- return "Update the project main branch name, based on GIT information. Only for the first project's analysis.";
- }
-}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStepTest.java
deleted file mode 100644
index ff18ae50338..00000000000
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStepTest.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info 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.
- */
-package org.sonar.ce.task.projectanalysis.step;
-
-import java.util.Collection;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.utils.System2;
-import org.sonar.api.utils.log.LogTester;
-import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
-import org.sonar.ce.task.projectanalysis.batch.BatchReportReader;
-import org.sonar.ce.task.step.ComputationStep;
-import org.sonar.db.DbTester;
-import org.sonar.db.component.BranchDto;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.scanner.protocol.output.ScannerReport;
-import org.sonar.server.project.Project;
-
-import static java.util.Collections.emptyList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.api.utils.log.LoggerLevel.TRACE;
-
-public class UpdateMainBranchStepTest {
-
- @Rule
- public DbTester dbTester = DbTester.create(System2.INSTANCE);
-
- @Rule
- public LogTester logTester = new LogTester().setLevel(TRACE);
-
- private final AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class);
- BatchReportReader batchReportReader = mock(BatchReportReader.class);
-
- private final UpdateMainBranchStep underTest = new UpdateMainBranchStep(batchReportReader, dbTester.getDbClient(), analysisMetadataHolder);
- private ComputationStep.Context context = mock(ComputationStep.Context.class);
-
- @Test
- public void update_main_branch_on_first_analysis() {
- ScannerReport.Metadata metadata = ScannerReport.Metadata.newBuilder().setGitDefaultMainBranch("new_name").buildPartial();
- when(batchReportReader.readMetadata()).thenReturn(metadata);
- when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true);
- ComponentDto privateProject = dbTester.components().insertPrivateProject();
- when(analysisMetadataHolder.getProject())
- .thenReturn(new Project(privateProject.uuid(), privateProject.getKey(), privateProject.name(), privateProject.description(), emptyList()));
-
- assertMainBranchName(privateProject, "master");
-
- underTest.execute(context);
-
- assertMainBranchName(privateProject, "new_name");
- assertThat(logTester.logs()).contains("GIT default main branch detected is [new_name]");
- assertThat(logTester.logs()).contains(String.format("updating project [%s] default main branch to [new_name]", privateProject.getKey()));
- }
-
- @Test
- public void do_not_update_main_branch_on_second_analysis() {
- ScannerReport.Metadata metadata = ScannerReport.Metadata.newBuilder().setGitDefaultMainBranch("new_name").buildPartial();
- when(batchReportReader.readMetadata()).thenReturn(metadata);
- when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
- ComponentDto privateProject = dbTester.components().insertPrivateProject();
- when(analysisMetadataHolder.getProject())
- .thenReturn(new Project(privateProject.uuid(), privateProject.getKey(), privateProject.name(), privateProject.description(), emptyList()));
-
- assertMainBranchName(privateProject, "master");
-
- underTest.execute(context);
-
- assertMainBranchName(privateProject, "master");
- assertThat(logTester.logs()).isEmpty();
- }
-
- @Test
- public void do_not_update_main_branch_if_no_git_info_found() {
- String emptyGitMainBranchInfo = "";
- ScannerReport.Metadata metadata = ScannerReport.Metadata.newBuilder().setGitDefaultMainBranch(emptyGitMainBranchInfo).buildPartial();
- when(batchReportReader.readMetadata()).thenReturn(metadata);
- when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true);
- ComponentDto privateProject = dbTester.components().insertPrivateProject();
- when(analysisMetadataHolder.getProject())
- .thenReturn(new Project(privateProject.uuid(), privateProject.getKey(), privateProject.name(), privateProject.description(), emptyList()));
-
- assertMainBranchName(privateProject, "master");
-
- underTest.execute(context);
-
- assertMainBranchName(privateProject, "master");
- assertThat(logTester.logs()).contains("GIT default main branch detected is empty");
- }
-
- @Test
- public void fail_on_invalid_project_key() {
- ScannerReport.Metadata metadata = ScannerReport.Metadata.newBuilder().setGitDefaultMainBranch("new_name").buildPartial();
- when(batchReportReader.readMetadata()).thenReturn(metadata);
- when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true);
- ComponentDto privateProject = dbTester.components().insertPrivateProject();
- when(analysisMetadataHolder.getProject())
- .thenReturn(new Project(privateProject.uuid(), "invalid project key", privateProject.name(), privateProject.description(), emptyList()));
-
- assertThatThrownBy(() -> underTest.execute(context))
- .isInstanceOf(IllegalStateException.class)
- .hasMessage("root component key [invalid project key] is not a project");
- assertThat(logTester.logs()).contains("GIT default main branch detected is [new_name]");
- }
-
- @Test
- public void getDescription() {
- assertThat(underTest.getDescription()).isNotEmpty();
- }
-
- private void assertMainBranchName(ComponentDto privateProject, String expectedBranchName) {
- Collection<BranchDto> branches = dbTester.getDbClient().branchDao().selectByComponent(dbTester.getSession(), privateProject);
- assertThat(branches).isNotEmpty();
- assertThat(branches).hasSize(1);
- assertThat(branches.iterator().next().getKey()).isEqualTo(expectedBranchName);
- }
-}
diff --git a/server/sonar-docs/src/pages/analysis/branch-pr-analysis-overview.md b/server/sonar-docs/src/pages/analysis/branch-pr-analysis-overview.md
index 91cdb97fae9..8335bf63c5f 100644
--- a/server/sonar-docs/src/pages/analysis/branch-pr-analysis-overview.md
+++ b/server/sonar-docs/src/pages/analysis/branch-pr-analysis-overview.md
@@ -10,6 +10,16 @@ SonarScanners running in GitLab CI/CD, Azure Pipelines, Cirrus CI, and Jenkins w
[[warning]]
| Automatic configuration is disabled if any branch or pull request properties have been set manually.
+## Keeping your "master" branch history when upgrading from Community Edition to a commercial edition
+
+In Community Edition, your analyzed branch is named "master" by default.
+
+When upgrading to a current commercial edition version, automatic branch and pull request configuration creates branches based on their names in your code repository. If the name of your Main Branch (master) in SonarQube doesn't match the branch's name in your code repository, the history of your Main Branch won't be taken on by the branch you analyze.
+
+**Before running analysis**, you can keep your branch history by renaming the Main Branch in SonarQube with the name of the branch in your code repository at **Project Settings > Branches and Pull Requests**.
+
+For example, if your Main Branch is named "master" in SonarQube but "develop" in your code repository, rename your Main Branch "develop" in SonarQube.
+
## GitLab CI/CD
For GitLab CI/CD configuration, see the [GitLab ALM integration](/analysis/gitlab-integration/) page.
diff --git a/server/sonar-docs/src/pages/branches/overview.md b/server/sonar-docs/src/pages/branches/overview.md
index c95f0e1e2f8..83c2660114f 100644
--- a/server/sonar-docs/src/pages/branches/overview.md
+++ b/server/sonar-docs/src/pages/branches/overview.md
@@ -7,19 +7,13 @@ _Branch analysis is available starting in [Developer Edition](https://redirect.s
## Overview
-With branch analysis, you can ensure that you're maintaining consistent code quality all the way down to the branch level of your projects.
+With Branch Analysis, you can ensure that you're maintaining consistent code quality all the way down to the branch level of your projects.
-### Master/Main branch
+### Master / Main Branch
-This is the default branch and typically corresponds to what's being developed for your next release. This is usually known within a development team as "master" or "head" and is analyzed when no specific branch parameters are provided. It is labeled **MAIN BRANCH** in SonarQube.
+This is the default branch and typically corresponds to what's being developed for your next release. This is usually known within a development team as "master" or "head" and is analyzed when no specific branch parameters are provided. It is labeled "Main Branch" and defaults to the name "master" but can be renamed from the project settings at **Administration > Branches and Pull Requests**. When you are using Community Edition, this is the only branch you see.
-If you're using Git as your SCM, when you analyze your project for the first time, the main branch will be renamed to match the main branch of your repository. If SonarQube can't find information about the main branch during the first analysis, it's named "master" by default.
-
-Starting in [Developer Edition](https://redirect.sonarsource.com/editions/developer.html), you can rename the main branch manually from the project settings at **Administration > Branches and Pull Requests**.
-
-In Community Edition, this is the only branch you see. You can rename it using the [Web API](/extend/web-api/).
-
-### Settings and quality profiles on branches
+### Settings and Quality Profiles on Branches
Branch settings and Quality Profiles are the same as those set for the master branch, and by design, it's not possible to configure other values. The New Code Period is the only exception to this as it can be set on a branch-by-branch basis.
@@ -34,7 +28,7 @@ The branch Quality Gate lets you know if your branch is ready to be merged. Each
* Applies on conditions on New Code and overall code.
* Assigns a status (Passed or Failed).
-## Setting up branch analysis
+## Setting up Branch analysis
A branch is created when the `sonar.branch.name` parameter is passed during analysis.
@@ -54,9 +48,9 @@ if [[ "$CI_BRANCH_NAME" == master ]] || [[ "$CI_BRANCH_NAME" == release/* ]]; th
fi
```
-### Issue Creation and synchronization
+### Issue Creation and Synchronization
-During the first analysis, issues (type, severity, status, assignee, change log, comments) are synchronized with the main branch. In each synchronized issue, a comment is added to the change log of the issue on the branch: "The issue has been copied from branch 'master' to branch yyy".
+During the first analysis, issues (type, severity, status, assignee, change log, comments) are synchronized with the Main Branch. In each synchronized issue, a comment is added to the change log of the issue on the branch: "The issue has been copied from branch 'master' to branch yyy".
At each subsequent analysis of the branch, any new issue that comes from a pull request automatically inherits the attributes (type, severity, ...) the issue had in the pull request. A comment is added to the change log of the issue on the branch: "The issue has been merged from 'xxx' into 'yyy'"
@@ -84,11 +78,3 @@ For example, adding the pattern `release/.*` would keep any branches named relea
You can set a branch to **Keep when inactive** at the project level from from the **Branches** tab at **Project Settings > Branches and Pull Requests**. Here, you can also turn off protection for a branch so it will be deleted when it's inactive for the number of days that has been specified in the global settings at **Administration > General Settings > Housekeeping > Number of days before deleting inactive branches**.
**Note:** The main branch is always protected from automatic deletion, even if it's inactive. This can't be changed.
-
-## Keeping your "master" branch history when upgrading from Community Edition to a commercial edition
-
-When upgrading to a current commercial edition version, automatic branch and pull request configuration creates branches based on their names in your code repository. If the name of your main branch (master) in SonarQube doesn't match the branch's name in your code repository, the history of your main branch won't be taken on by the branch you analyze.
-
-**Before running analysis**, you can keep your branch history by renaming the main branch in SonarQube with the name of the branch in your code repository at **Project Settings > Branches and Pull Requests**.
-
-For example, if your main branch is named "master" in SonarQube but "develop" in your code repository, rename your main branch "develop" in SonarQube.
diff --git a/server/sonar-web/src/main/js/api/news.ts b/server/sonar-web/src/main/js/api/news.ts
new file mode 100644
index 00000000000..3b4e00488d0
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/news.ts
@@ -0,0 +1,139 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info 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 { getCorsJSON } from 'sonar-ui-common/helpers/request';
+
+interface PrismicRef {
+ id: string;
+ ref: string;
+}
+
+export interface PrismicNews {
+ data: { title: string };
+ last_publication_date: string;
+ uid: string;
+}
+
+interface PrismicResponse {
+ page: number;
+ results: PrismicResult[];
+ results_per_page: number;
+ total_results_size: number;
+}
+
+interface PrismicResult {
+ data: {
+ notification: string;
+ publication_date: string;
+ body: PrismicResultFeature[];
+ };
+}
+
+interface PrismicResultFeature {
+ items: Array<{
+ category: {
+ data: {
+ color: string;
+ name: string;
+ };
+ };
+ }>;
+ primary: {
+ description: string;
+ read_more_link: {
+ url?: string;
+ };
+ };
+}
+
+export interface PrismicFeatureNews {
+ notification: string;
+ publicationDate: string;
+ features: Array<{
+ categories: Array<{
+ color: string;
+ name: string;
+ }>;
+ description: string;
+ readMore?: string;
+ }>;
+}
+
+const PRISMIC_API_URL = 'https://sonarsource.cdn.prismic.io/api/v2';
+
+export function fetchPrismicRefs() {
+ return getCorsJSON(PRISMIC_API_URL).then((response: { refs: PrismicRef[] }) => {
+ const master = response && response.refs.find(ref => ref.id === 'master');
+ if (!master) {
+ return Promise.reject('No master ref found');
+ }
+ return master;
+ });
+}
+
+export function fetchPrismicNews(data: {
+ accessToken: string;
+ ps?: number;
+ ref: string;
+ tag?: string;
+}) {
+ const q = ['[[at(document.type, "blog_sonarsource_post")]]'];
+ if (data.tag) {
+ q.push(`[[at(document.tags,["${data.tag}"])]]`);
+ }
+ return getCorsJSON(PRISMIC_API_URL + '/documents/search', {
+ access_token: data.accessToken,
+ orderings: '[document.first_publication_date desc]',
+ pageSize: data.ps || 1,
+ q,
+ ref: data.ref
+ }).then(({ results }: { results: PrismicNews[] }) => results);
+}
+
+export function fetchPrismicFeatureNews(data: {
+ accessToken: string;
+ p?: number;
+ ps?: number;
+ ref: string;
+}): Promise<{ news: PrismicFeatureNews[]; paging: T.Paging }> {
+ return getCorsJSON(PRISMIC_API_URL + '/documents/search', {
+ access_token: data.accessToken,
+ fetchLinks: 'sc_category.color,sc_category.name',
+ orderings: '[my.sc_product_news.publication_date desc]',
+ page: data.p || 1,
+ pageSize: data.ps || 1,
+ q: ['[[at(document.type, "sc_product_news")]]'],
+ ref: data.ref
+ }).then(({ page, results, results_per_page, total_results_size }: PrismicResponse) => ({
+ news: results.map(result => ({
+ notification: result.data.notification,
+ publicationDate: result.data.publication_date,
+ features: result.data.body.map(feature => ({
+ categories: feature.items.map(item => item.category.data).filter(Boolean),
+ description: feature.primary.description,
+ readMore: feature.primary.read_more_link.url
+ }))
+ })),
+ paging: {
+ pageIndex: page,
+ pageSize: results_per_page,
+ total: total_results_size
+ }
+ }));
+}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
index ab3f44270e7..d5e145cbeea 100644
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
@@ -22,6 +22,8 @@ import { Link } from 'react-router';
import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
+import { isSonarCloud } from '../../../helpers/system';
+import ProductNewsMenuItem from './ProductNewsMenuItem';
import { SuggestionsContext } from './SuggestionsContext';
interface Props {
@@ -67,6 +69,70 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
);
}
+ renderSonarCloudLinks() {
+ return (
+ <>
+ <li className="divider" />
+ <li>
+ <a
+ href="https://community.sonarsource.com/c/help/sc"
+ rel="noopener noreferrer"
+ target="_blank">
+ {translate('embed_docs.get_help')}
+ </a>
+ </li>
+ <li className="divider" />
+ {this.renderTitle(translate('embed_docs.stay_connected'))}
+ <li>
+ {this.renderIconLink(
+ 'https://twitter.com/sonarcloud',
+ 'embed-doc/twitter-icon.svg',
+ 'Twitter'
+ )}
+ </li>
+ <li>
+ {this.renderIconLink(
+ 'https://blog.sonarsource.com/product/SonarCloud',
+ 'sonarcloud-square-logo.svg',
+ translate('embed_docs.blog')
+ )}
+ </li>
+ <li>
+ <ProductNewsMenuItem tag="SonarCloud" />
+ </li>
+ </>
+ );
+ }
+
+ renderSonarQubeLinks() {
+ return (
+ <>
+ <li className="divider" />
+ <li>
+ <a href="https://community.sonarsource.com/" rel="noopener noreferrer" target="_blank">
+ {translate('embed_docs.get_help')}
+ </a>
+ </li>
+ <li className="divider" />
+ {this.renderTitle(translate('embed_docs.stay_connected'))}
+ <li>
+ {this.renderIconLink(
+ 'https://www.sonarqube.org/whats-new/?referrer=sonarqube',
+ 'embed-doc/sq-icon.svg',
+ translate('embed_docs.news')
+ )}
+ </li>
+ <li>
+ {this.renderIconLink(
+ 'https://twitter.com/SonarQube',
+ 'embed-doc/twitter-icon.svg',
+ 'Twitter'
+ )}
+ </li>
+ </>
+ );
+ }
+
render() {
return (
<DropdownOverlay>
@@ -82,28 +148,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
{translate('api_documentation.page')}
</Link>
</li>
- <li className="divider" />
- <li>
- <a href="https://community.sonarsource.com/" rel="noopener noreferrer" target="_blank">
- {translate('embed_docs.get_help')}
- </a>
- </li>
- <li className="divider" />
- {this.renderTitle(translate('embed_docs.stay_connected'))}
- <li>
- {this.renderIconLink(
- 'https://www.sonarqube.org/whats-new/?referrer=sonarqube',
- 'embed-doc/sq-icon.svg',
- translate('embed_docs.news')
- )}
- </li>
- <li>
- {this.renderIconLink(
- 'https://twitter.com/SonarQube',
- 'embed-doc/twitter-icon.svg',
- 'Twitter'
- )}
- </li>
+ {isSonarCloud() ? this.renderSonarCloudLinks() : this.renderSonarQubeLinks()}
</ul>
</DropdownOverlay>
);
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx
new file mode 100644
index 00000000000..62ed86eae5f
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info 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 * as React from 'react';
+import { connect } from 'react-redux';
+import ChevronRightIcon from 'sonar-ui-common/components/icons/ChevronRightIcon';
+import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { fetchPrismicNews, fetchPrismicRefs, PrismicNews } from '../../../api/news';
+import PlaceholderBar from '../../../components/ui/PlaceholderBar';
+import { getGlobalSettingValue, Store } from '../../../store/rootReducer';
+
+interface OwnProps {
+ tag?: string;
+}
+
+interface StateProps {
+ accessToken?: string;
+}
+
+type Props = OwnProps & StateProps;
+
+interface State {
+ loading: boolean;
+ news?: PrismicNews;
+}
+
+export class ProductNewsMenuItem extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchProductNews();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchProductNews = () => {
+ const { accessToken, tag } = this.props;
+ if (accessToken) {
+ this.setState({ loading: true });
+ fetchPrismicRefs()
+ .then(({ ref }) => fetchPrismicNews({ accessToken, ref, tag }))
+ .then(
+ news => {
+ if (this.mounted) {
+ this.setState({ news: news[0], loading: false });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ }
+ };
+
+ renderPlaceholder() {
+ return (
+ <a className="rich-item new-loading">
+ <div className="flex-1">
+ <div className="display-inline-flex-center">
+ <h4>{translate('embed_docs.latest_blog')}</h4>
+ <span className="note spacer-left">
+ <PlaceholderBar color="#aaa" width={60} />
+ </span>
+ </div>
+ <p className="little-spacer-bottom">
+ <PlaceholderBar color="#aaa" width={84} /> <PlaceholderBar color="#aaa" width={48} />{' '}
+ <PlaceholderBar color="#aaa" width={24} /> <PlaceholderBar color="#aaa" width={72} />{' '}
+ <PlaceholderBar color="#aaa" width={24} /> <PlaceholderBar color="#aaa" width={48} />
+ </p>
+ </div>
+ <ChevronRightIcon className="flex-0" />
+ </a>
+ );
+ }
+
+ render() {
+ const link = 'https://blog.sonarsource.com/';
+ const { loading, news } = this.state;
+
+ if (loading) {
+ return this.renderPlaceholder();
+ }
+
+ if (!news) {
+ return null;
+ }
+
+ return (
+ <a className="rich-item" href={link + news.uid} rel="noopener noreferrer" target="_blank">
+ <div className="flex-1">
+ <div className="display-inline-flex-center">
+ <h4>{translate('embed_docs.latest_blog')}</h4>
+ <DateFormatter date={news.last_publication_date}>
+ {formattedDate => <span className="note spacer-left">{formattedDate}</span>}
+ </DateFormatter>
+ </div>
+ <p className="little-spacer-bottom">{news.data.title}</p>
+ </div>
+ <ChevronRightIcon className="flex-0" />
+ </a>
+ );
+ }
+}
+
+const mapStateToProps = (state: Store): StateProps => {
+ const accessToken = getGlobalSettingValue(state, 'sonar.prismic.accessToken');
+ return {
+ accessToken: accessToken && accessToken.value
+ };
+};
+
+export default connect(mapStateToProps)(ProductNewsMenuItem);
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx
new file mode 100644
index 00000000000..a6605fb1d6a
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { fetchPrismicNews, fetchPrismicRefs } from '../../../../api/news';
+import { ProductNewsMenuItem } from '../ProductNewsMenuItem';
+
+jest.mock('../../../../api/news', () => ({
+ fetchPrismicRefs: jest.fn().mockResolvedValue({ id: 'master', ref: 'master-ref' }),
+ fetchPrismicNews: jest.fn().mockResolvedValue([
+ {
+ data: { title: 'My Product News' },
+ last_publication_date: '2018-04-06T12:07:19+0000',
+ uid: 'my-product-news'
+ }
+ ])
+}));
+
+it('should load the product news', async () => {
+ const wrapper = shallow(<ProductNewsMenuItem accessToken="token" tag="SonarCloud" />);
+ expect(wrapper).toMatchSnapshot();
+ await waitAndUpdate(wrapper);
+ expect(fetchPrismicRefs).toHaveBeenCalled();
+ expect(fetchPrismicNews).toHaveBeenCalledWith({
+ accessToken: 'token',
+ ref: 'master-ref',
+ tag: 'SonarCloud'
+ });
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap
new file mode 100644
index 00000000000..2cd910101ba
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap
@@ -0,0 +1,97 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should load the product news 1`] = `
+<a
+ className="rich-item new-loading"
+>
+ <div
+ className="flex-1"
+ >
+ <div
+ className="display-inline-flex-center"
+ >
+ <h4>
+ embed_docs.latest_blog
+ </h4>
+ <span
+ className="note spacer-left"
+ >
+ <PlaceholderBar
+ color="#aaa"
+ width={60}
+ />
+ </span>
+ </div>
+ <p
+ className="little-spacer-bottom"
+ >
+ <PlaceholderBar
+ color="#aaa"
+ width={84}
+ />
+
+ <PlaceholderBar
+ color="#aaa"
+ width={48}
+ />
+
+ <PlaceholderBar
+ color="#aaa"
+ width={24}
+ />
+
+ <PlaceholderBar
+ color="#aaa"
+ width={72}
+ />
+
+ <PlaceholderBar
+ color="#aaa"
+ width={24}
+ />
+
+ <PlaceholderBar
+ color="#aaa"
+ width={48}
+ />
+ </p>
+ </div>
+ <ChevronRightIcon
+ className="flex-0"
+ />
+</a>
+`;
+
+exports[`should load the product news 2`] = `
+<a
+ className="rich-item"
+ href="https://blog.sonarsource.com/my-product-news"
+ rel="noopener noreferrer"
+ target="_blank"
+>
+ <div
+ className="flex-1"
+ >
+ <div
+ className="display-inline-flex-center"
+ >
+ <h4>
+ embed_docs.latest_blog
+ </h4>
+ <DateFormatter
+ date="2018-04-06T12:07:19+0000"
+ >
+ <Component />
+ </DateFormatter>
+ </div>
+ <p
+ className="little-spacer-bottom"
+ >
+ My Product News
+ </p>
+ </div>
+ <ChevronRightIcon
+ className="flex-0"
+ />
+</a>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
index cafece6895c..72729ff47ce 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
@@ -57,11 +57,6 @@ export function BranchLikeNavigation(props: BranchLikeNavigationProps) {
/>
);
- // Main branch hasn't been analyzed yet && (CE || (DE+ && only one branch))
- if (!component.analysisDate && (!branchesEnabled || !hasManyBranches)) {
- return null;
- }
-
return (
<span
className={classNames('big-spacer-left flex-0 branch-like-navigation-toggler-container', {
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
index ff00b6d7624..c0b34d01686 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
@@ -30,23 +30,6 @@ it('should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
-it('should not render', () => {
- // CE && main branch not analyzed yet
- const wrapper = shallowRender({
- appState: mockAppState({ branchesEnabled: false }),
- component: mockComponent({ analysisDate: undefined })
- });
- expect(wrapper.type()).toBeNull();
-
- // DE+ && main branch not analyzed yet && no other branches
- const wrapper1 = shallowRender({
- appState: mockAppState({ branchesEnabled: true }),
- component: mockComponent({ analysisDate: undefined }),
- branchLikes: []
- });
- expect(wrapper1.type()).toBeNull();
-});
-
it('should render the menu trigger if branches are enabled', () => {
const wrapper = shallowRender({ appState: mockAppState({ branchesEnabled: true }) });
expect(wrapper).toMatchSnapshot();
@@ -84,7 +67,7 @@ function shallowRender(props?: Partial<BranchLikeNavigationProps>) {
<BranchLikeNavigation
appState={mockAppState()}
branchLikes={branchLikes}
- component={mockComponent({ analysisDate: '2021-01-01 01:01:01' })}
+ component={mockComponent()}
currentBranchLike={branchLikes[0]}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
index 5e1897ca5c1..9a956e3303c 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
@@ -8,7 +8,6 @@ exports[`should render correctly 1`] = `
branchesEnabled={false}
component={
Object {
- "analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
@@ -118,7 +117,6 @@ exports[`should render the menu trigger if branches are enabled 1`] = `
}
component={
Object {
- "analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
@@ -160,7 +158,6 @@ exports[`should render the menu trigger if branches are enabled 1`] = `
branchesEnabled={true}
component={
Object {
- "analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
diff --git a/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx b/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
new file mode 100644
index 00000000000..30d49578d50
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info 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 * as differenceInSeconds from 'date-fns/difference_in_seconds';
+import * as React from 'react';
+import ClearIcon from 'sonar-ui-common/components/icons/ClearIcon';
+import NotificationIcon from 'sonar-ui-common/components/icons/NotificationIcon';
+import { parseDate } from 'sonar-ui-common/helpers/dates';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { PrismicFeatureNews } from '../../../api/news';
+import './notifications.css';
+
+interface Props {
+ lastNews: PrismicFeatureNews;
+ notificationsLastReadDate?: Date;
+ notificationsOptOut?: boolean;
+ onClick: () => void;
+ setCurrentUserSetting: (setting: T.CurrentUserSetting) => void;
+}
+
+export default class NavLatestNotification extends React.PureComponent<Props> {
+ mounted = false;
+
+ checkHasUnread = () => {
+ const { notificationsLastReadDate, lastNews } = this.props;
+ return (
+ !notificationsLastReadDate ||
+ differenceInSeconds(parseDate(lastNews.publicationDate), notificationsLastReadDate) > 0
+ );
+ };
+
+ handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onClick();
+ };
+
+ handleDismiss = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.props.setCurrentUserSetting({
+ key: 'notifications.readDate',
+ value: Date.now().toString()
+ });
+ };
+
+ render() {
+ const { notificationsOptOut, lastNews } = this.props;
+ const hasUnread = this.checkHasUnread();
+ const showNotifications = Boolean(!notificationsOptOut && lastNews && hasUnread);
+ return (
+ <>
+ {showNotifications && (
+ <>
+ <li className="navbar-latest-notification" onClick={this.props.onClick}>
+ <div className="navbar-latest-notification-wrapper">
+ <span className="badge badge-info">{translate('new')}</span>
+ <span className="label">{lastNews.notification}</span>
+ </div>
+ </li>
+ <li className="navbar-latest-notification-dismiss">
+ <a className="navbar-icon" href="#" onClick={this.handleDismiss}>
+ <ClearIcon size={12} thin={true} />
+ </a>
+ </li>
+ </>
+ )}
+ <li>
+ <a className="navbar-icon" href="#" onClick={this.handleClick}>
+ <NotificationIcon hasUnread={hasUnread && !notificationsOptOut} />
+ </a>
+ </li>
+ </>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx b/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx
new file mode 100644
index 00000000000..92536433eb9
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx
@@ -0,0 +1,134 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info 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 * as classNames from 'classnames';
+import * as differenceInSeconds from 'date-fns/difference_in_seconds';
+import * as React from 'react';
+import { ClearButton } from 'sonar-ui-common/components/controls/buttons';
+import Modal from 'sonar-ui-common/components/controls/Modal';
+import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { PrismicFeatureNews } from '../../../api/news';
+
+export interface Props {
+ fetchMoreFeatureNews: () => void;
+ loading: boolean;
+ loadingMore: boolean;
+ news: PrismicFeatureNews[];
+ onClose: () => void;
+ notificationsLastReadDate?: Date;
+ paging?: T.Paging;
+}
+
+export default function NotificationsSidebar(props: Props) {
+ const { loading, loadingMore, news, notificationsLastReadDate, paging } = props;
+ const header = translate('embed_docs.whats_new');
+ return (
+ <Modal contentLabel={header} onRequestClose={props.onClose}>
+ <div className="notifications-sidebar">
+ <div className="notifications-sidebar-top">
+ <h3>{header}</h3>
+ <ClearButton
+ className="button-tiny"
+ iconProps={{ size: 12, thin: true }}
+ onClick={props.onClose}
+ />
+ </div>
+ <div className="notifications-sidebar-content">
+ {loading ? (
+ <div className="text-center">
+ <DeferredSpinner className="big-spacer-top" timeout={200} />
+ </div>
+ ) : (
+ news.map((slice, index) => (
+ <Notification
+ key={slice.publicationDate}
+ notification={slice}
+ unread={isUnread(index, slice.publicationDate, notificationsLastReadDate)}
+ />
+ ))
+ )}
+ </div>
+ {!loading && paging && paging.total > news.length && (
+ <div className="notifications-sidebar-footer">
+ <div className="spacer-top note text-center">
+ <a className="spacer-left" href="#" onClick={props.fetchMoreFeatureNews}>
+ {translate('show_more')}
+ </a>
+ {loadingMore && (
+ <DeferredSpinner className="text-bottom spacer-left position-absolute" />
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ </Modal>
+ );
+}
+
+export function isUnread(index: number, notificationDate: string, lastReadDate?: Date) {
+ return !lastReadDate ? index < 1 : differenceInSeconds(notificationDate, lastReadDate) > 0;
+}
+
+interface NotificationProps {
+ notification: PrismicFeatureNews;
+ unread: boolean;
+}
+
+export function Notification({ notification, unread }: NotificationProps) {
+ return (
+ <div className={classNames('notifications-sidebar-slice', { unread })}>
+ <h4>
+ <DateFormatter date={notification.publicationDate} long={false} />
+ </h4>
+ {notification.features.map((feature, index) => (
+ <Feature feature={feature} key={index} />
+ ))}
+ </div>
+ );
+}
+
+interface FeatureProps {
+ feature: PrismicFeatureNews['features'][0];
+}
+
+export function Feature({ feature }: FeatureProps) {
+ return (
+ <div className="feature">
+ <ul className="categories spacer-bottom">
+ {feature.categories.map(category => (
+ <li key={category.name} style={{ backgroundColor: category.color }}>
+ {category.name}
+ </li>
+ ))}
+ </ul>
+ <span>{feature.description}</span>
+ {feature.readMore && (
+ <a
+ className="learn-more"
+ href={feature.readMore}
+ rel="noopener noreferrer nofollow"
+ target="_blank">
+ {translate('learn_more')}
+ </a>
+ )}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx b/server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx
new file mode 100644
index 00000000000..334563bdced
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { parseDate } from 'sonar-ui-common/helpers/dates';
+import { PrismicFeatureNews } from '../../../../api/news';
+import NavLatestNotification from '../NavLatestNotification';
+
+it('should render correctly if there are new features, and the user has not opted out', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('.navbar-latest-notification')).toHaveLength(1);
+});
+
+it('should render correctly if there are new features, but the user has opted out', () => {
+ const wrapper = shallowRender({ notificationsOptOut: true });
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('.navbar-latest-notification')).toHaveLength(0);
+});
+
+it('should render correctly if there are no new unread features', () => {
+ const wrapper = shallowRender({
+ notificationsLastReadDate: parseDate('2018-12-31T12:07:19+0000')
+ });
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('.navbar-latest-notification')).toHaveLength(0);
+});
+
+function shallowRender(props: Partial<NavLatestNotification['props']> = {}) {
+ const lastNews: PrismicFeatureNews = {
+ notification: '10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration',
+ publicationDate: '2018-04-06',
+ features: [
+ {
+ categories: [{ color: '#ff0000', name: 'Java' }],
+ description: '10 new Java rules'
+ }
+ ]
+ };
+ return shallow(
+ <NavLatestNotification
+ lastNews={lastNews}
+ notificationsLastReadDate={parseDate('2018-01-01T12:07:19+0000')}
+ notificationsOptOut={false}
+ onClick={jest.fn()}
+ setCurrentUserSetting={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx b/server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx
new file mode 100644
index 00000000000..f9b92ec27d0
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { parseDate } from 'sonar-ui-common/helpers/dates';
+import NotificationsSidebar, {
+ Feature,
+ isUnread,
+ Notification,
+ Props
+} from '../NotificationsSidebar';
+
+const news: Props['news'] = [
+ {
+ notification: '10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration',
+ publicationDate: '2018-04-06',
+ features: [
+ {
+ categories: [
+ { color: '#ff0000', name: 'Java' },
+ { color: '#00ff00', name: 'Rules' }
+ ],
+ description: '10 new Java rules'
+ },
+ {
+ categories: [{ color: '#0000ff', name: 'BitBucket' }],
+ description: 'BitBucket branch decoration',
+ readMore: 'http://example.com'
+ }
+ ]
+ },
+ {
+ notification: 'Some other notification',
+ publicationDate: '2018-04-05',
+ features: [
+ {
+ categories: [{ color: '#0000ff', name: 'BitBucket' }],
+ description: 'BitBucket branch decoration',
+ readMore: 'http://example.com'
+ }
+ ]
+ }
+];
+
+describe('#NotificationSidebar', () => {
+ it('should render correctly if there are new features', () => {
+ const wrapper = shallowRender({ loading: true });
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setProps({ loading: false });
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('Notification')).toHaveLength(2);
+ });
+
+ it('should render correctly if there are no new unread features', () => {
+ const wrapper = shallowRender({
+ notificationsLastReadDate: parseDate('2018-12-31')
+ });
+ expect(wrapper.find('Notification')).toHaveLength(2);
+ expect(wrapper.find('Notification[unread=true]')).toHaveLength(0);
+ });
+});
+
+describe('#isUnread', () => {
+ it('should be unread', () => {
+ expect(isUnread(0, '2018-12-14', undefined)).toBe(true);
+ expect(isUnread(1, '2018-12-14', parseDate('2018-12-12'))).toBe(true);
+ });
+
+ it('should be read', () => {
+ expect(isUnread(0, '2018-12-16', parseDate('2018-12-16'))).toBe(false);
+ expect(isUnread(1, '2018-12-15', undefined)).toBe(false);
+ });
+});
+
+describe('#Notification', () => {
+ it('should render correctly', () => {
+ expect(shallow(<Notification notification={news[1]} unread={false} />)).toMatchSnapshot();
+ expect(shallow(<Notification notification={news[1]} unread={true} />)).toMatchSnapshot();
+ });
+});
+
+describe('#Feature', () => {
+ it('should render correctly', () => {
+ expect(shallow(<Feature feature={news[1].features[0]} />)).toMatchSnapshot();
+ expect(shallow(<Feature feature={news[0].features[0]} />)).toMatchSnapshot();
+ });
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
+ <NotificationsSidebar
+ fetchMoreFeatureNews={jest.fn()}
+ loading={false}
+ loadingMore={false}
+ news={news}
+ notificationsLastReadDate={parseDate('2018-01-01')}
+ onClose={jest.fn()}
+ paging={{ pageIndex: 1, pageSize: 10, total: 20 }}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
new file mode 100644
index 00000000000..c748dbfcf16
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
@@ -0,0 +1,82 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly if there are new features, and the user has not opted out 1`] = `
+<Fragment>
+ <li
+ className="navbar-latest-notification"
+ onClick={[MockFunction]}
+ >
+ <div
+ className="navbar-latest-notification-wrapper"
+ >
+ <span
+ className="badge badge-info"
+ >
+ new
+ </span>
+ <span
+ className="label"
+ >
+ 10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration
+ </span>
+ </div>
+ </li>
+ <li
+ className="navbar-latest-notification-dismiss"
+ >
+ <a
+ className="navbar-icon"
+ href="#"
+ onClick={[Function]}
+ >
+ <ClearIcon
+ size={12}
+ thin={true}
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ className="navbar-icon"
+ href="#"
+ onClick={[Function]}
+ >
+ <NotificationIcon
+ hasUnread={true}
+ />
+ </a>
+ </li>
+</Fragment>
+`;
+
+exports[`should render correctly if there are new features, but the user has opted out 1`] = `
+<Fragment>
+ <li>
+ <a
+ className="navbar-icon"
+ href="#"
+ onClick={[Function]}
+ >
+ <NotificationIcon
+ hasUnread={false}
+ />
+ </a>
+ </li>
+</Fragment>
+`;
+
+exports[`should render correctly if there are no new unread features 1`] = `
+<Fragment>
+ <li>
+ <a
+ className="navbar-icon"
+ href="#"
+ onClick={[Function]}
+ >
+ <NotificationIcon
+ hasUnread={false}
+ />
+ </a>
+ </li>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap
new file mode 100644
index 00000000000..4936ee19ea8
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap
@@ -0,0 +1,269 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`#Feature should render correctly 1`] = `
+<div
+ className="feature"
+>
+ <ul
+ className="categories spacer-bottom"
+ >
+ <li
+ key="BitBucket"
+ style={
+ Object {
+ "backgroundColor": "#0000ff",
+ }
+ }
+ >
+ BitBucket
+ </li>
+ </ul>
+ <span>
+ BitBucket branch decoration
+ </span>
+ <a
+ className="learn-more"
+ href="http://example.com"
+ rel="noopener noreferrer nofollow"
+ target="_blank"
+ >
+ learn_more
+ </a>
+</div>
+`;
+
+exports[`#Feature should render correctly 2`] = `
+<div
+ className="feature"
+>
+ <ul
+ className="categories spacer-bottom"
+ >
+ <li
+ key="Java"
+ style={
+ Object {
+ "backgroundColor": "#ff0000",
+ }
+ }
+ >
+ Java
+ </li>
+ <li
+ key="Rules"
+ style={
+ Object {
+ "backgroundColor": "#00ff00",
+ }
+ }
+ >
+ Rules
+ </li>
+ </ul>
+ <span>
+ 10 new Java rules
+ </span>
+</div>
+`;
+
+exports[`#Notification should render correctly 1`] = `
+<div
+ className="notifications-sidebar-slice"
+>
+ <h4>
+ <DateFormatter
+ date="2018-04-05"
+ long={false}
+ />
+ </h4>
+ <Feature
+ feature={
+ Object {
+ "categories": Array [
+ Object {
+ "color": "#0000ff",
+ "name": "BitBucket",
+ },
+ ],
+ "description": "BitBucket branch decoration",
+ "readMore": "http://example.com",
+ }
+ }
+ key="0"
+ />
+</div>
+`;
+
+exports[`#Notification should render correctly 2`] = `
+<div
+ className="notifications-sidebar-slice unread"
+>
+ <h4>
+ <DateFormatter
+ date="2018-04-05"
+ long={false}
+ />
+ </h4>
+ <Feature
+ feature={
+ Object {
+ "categories": Array [
+ Object {
+ "color": "#0000ff",
+ "name": "BitBucket",
+ },
+ ],
+ "description": "BitBucket branch decoration",
+ "readMore": "http://example.com",
+ }
+ }
+ key="0"
+ />
+</div>
+`;
+
+exports[`#NotificationSidebar should render correctly if there are new features 1`] = `
+<Modal
+ contentLabel="embed_docs.whats_new"
+ onRequestClose={[MockFunction]}
+>
+ <div
+ className="notifications-sidebar"
+ >
+ <div
+ className="notifications-sidebar-top"
+ >
+ <h3>
+ embed_docs.whats_new
+ </h3>
+ <ClearButton
+ className="button-tiny"
+ iconProps={
+ Object {
+ "size": 12,
+ "thin": true,
+ }
+ }
+ onClick={[MockFunction]}
+ />
+ </div>
+ <div
+ className="notifications-sidebar-content"
+ >
+ <div
+ className="text-center"
+ >
+ <DeferredSpinner
+ className="big-spacer-top"
+ timeout={200}
+ />
+ </div>
+ </div>
+ </div>
+</Modal>
+`;
+
+exports[`#NotificationSidebar should render correctly if there are new features 2`] = `
+<Modal
+ contentLabel="embed_docs.whats_new"
+ onRequestClose={[MockFunction]}
+>
+ <div
+ className="notifications-sidebar"
+ >
+ <div
+ className="notifications-sidebar-top"
+ >
+ <h3>
+ embed_docs.whats_new
+ </h3>
+ <ClearButton
+ className="button-tiny"
+ iconProps={
+ Object {
+ "size": 12,
+ "thin": true,
+ }
+ }
+ onClick={[MockFunction]}
+ />
+ </div>
+ <div
+ className="notifications-sidebar-content"
+ >
+ <Notification
+ key="2018-04-06"
+ notification={
+ Object {
+ "features": Array [
+ Object {
+ "categories": Array [
+ Object {
+ "color": "#ff0000",
+ "name": "Java",
+ },
+ Object {
+ "color": "#00ff00",
+ "name": "Rules",
+ },
+ ],
+ "description": "10 new Java rules",
+ },
+ Object {
+ "categories": Array [
+ Object {
+ "color": "#0000ff",
+ "name": "BitBucket",
+ },
+ ],
+ "description": "BitBucket branch decoration",
+ "readMore": "http://example.com",
+ },
+ ],
+ "notification": "10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration",
+ "publicationDate": "2018-04-06",
+ }
+ }
+ unread={true}
+ />
+ <Notification
+ key="2018-04-05"
+ notification={
+ Object {
+ "features": Array [
+ Object {
+ "categories": Array [
+ Object {
+ "color": "#0000ff",
+ "name": "BitBucket",
+ },
+ ],
+ "description": "BitBucket branch decoration",
+ "readMore": "http://example.com",
+ },
+ ],
+ "notification": "Some other notification",
+ "publicationDate": "2018-04-05",
+ }
+ }
+ unread={true}
+ />
+ </div>
+ <div
+ className="notifications-sidebar-footer"
+ >
+ <div
+ className="spacer-top note text-center"
+ >
+ <a
+ className="spacer-left"
+ href="#"
+ onClick={[MockFunction]}
+ >
+ show_more
+ </a>
+ </div>
+ </div>
+ </div>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/notifications/notifications.css b/server/sonar-web/src/main/js/app/components/notifications/notifications.css
new file mode 100644
index 00000000000..ef18020bf4f
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/notifications.css
@@ -0,0 +1,157 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info 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.
+ */
+.navbar-latest-notification {
+ flex: 0 1 240px;
+ text-align: right;
+ overflow: hidden;
+}
+
+.navbar-latest-notification-wrapper {
+ position: relative;
+ display: inline-block;
+ padding: var(--gridSize);
+ padding-left: 50px;
+ height: 28px;
+ max-width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
+ vertical-align: middle;
+ font-size: var(--smallFontSize);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--sonarcloudBlack500);
+ background-color: #000;
+ border-radius: 3px 0 0 3px;
+ cursor: pointer;
+}
+
+.navbar-latest-notification-wrapper:hover {
+ color: var(--sonarcloudBlack300);
+}
+
+.navbar-latest-notification-wrapper .badge-info {
+ position: absolute;
+ margin-right: var(--gridSize);
+ left: 6px;
+ top: 6px;
+}
+
+.navbar-latest-notification-wrapper .label {
+ display: block;
+ max-width: 330px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.navbar-latest-notification-dismiss .navbar-icon {
+ height: 28px;
+ background-color: #000;
+ border-radius: 0 3px 3px 0;
+ padding: var(--gridSize) 7px !important;
+ margin-left: 1px;
+ margin-right: var(--gridSize);
+ color: var(--sonarcloudBlack500) !important;
+}
+
+.navbar-latest-notification-dismiss .navbar-icon:hover {
+ color: var(--sonarcloudBlack300) !important;
+}
+
+.notifications-sidebar {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 400px;
+ display: flex;
+ flex-direction: column;
+ background: var(--sonarcloudBlack200);
+}
+
+.notifications-sidebar-top {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: calc(2 * var(--gridSize));
+ border-bottom: 1px solid var(--sonarcloudBlack250);
+ background-color: var(--sonarcloudBlack100);
+}
+
+.notifications-sidebar-top h3 {
+ font-weight: normal;
+ font-size: var(--bigFontSize);
+}
+
+.notifications-sidebar-content {
+ flex: 1 1;
+ overflow-y: auto;
+}
+
+.notifications-sidebar-footer {
+ padding-top: var(--gridSize);
+ border-top: 1px solid var(--sonarcloudBlack250);
+ flex: 0 0 40px;
+}
+
+.notifications-sidebar-slice h4 {
+ padding: calc(2 * var(--gridSize));
+ padding-bottom: calc(var(--gridSize) / 2);
+ background-color: var(--sonarcloudBlack200);
+ font-weight: normal;
+ font-size: var(--smallFontSize);
+ text-align: right;
+ color: var(--sonarcloudBlack500);
+}
+
+.notifications-sidebar-slice .feature:last-of-type {
+ border-bottom: 1px solid var(--sonarcloudBlack250);
+}
+
+.notifications-sidebar-slice .feature {
+ padding: calc(2 * var(--gridSize));
+ background-color: var(--sonarcloudBlack100);
+ border-top: 1px solid var(--sonarcloudBlack250);
+ overflow: hidden;
+}
+
+.notifications-sidebar-slice.unread .feature {
+ background-color: #e6f6ff;
+ border-color: #cee4f2;
+}
+
+.notifications-sidebar-slice .learn-more {
+ clear: both;
+ float: right;
+ margin-top: var(--gridSize);
+}
+
+.notifications-sidebar-slice .categories li {
+ display: inline-block;
+ padding: 4px;
+ margin-right: var(--gridSize);
+ font-size: 9px;
+ line-height: 8px;
+ text-transform: uppercase;
+ font-weight: bold;
+ color: #fff;
+ border-radius: 3px;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
index ed9efc5b521..32a1c54100d 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
@@ -34,7 +34,6 @@ import BranchList from './BranchList';
import ProjectBaselineSelector from './ProjectBaselineSelector';
interface Props {
- branchLike: Branch;
branchLikes: BranchLike[];
branchesEnabled?: boolean;
canAdmin?: boolean;
@@ -121,15 +120,13 @@ export default class App extends React.PureComponent<Props, State> {
}
fetchLeakPeriodSetting() {
- const { branchLike, branchesEnabled, component } = this.props;
-
this.setState({ loading: true });
Promise.all([
getNewCodePeriod(),
getNewCodePeriod({
- branch: branchesEnabled ? undefined : branchLike.name,
- project: component.key
+ branch: !this.props.branchesEnabled ? 'master' : undefined,
+ project: this.props.component.key
})
]).then(
([generalSetting, setting]) => {
@@ -229,7 +226,7 @@ export default class App extends React.PureComponent<Props, State> {
};
render() {
- const { branchesEnabled, canAdmin, component, branchLike } = this.props;
+ const { branchesEnabled, canAdmin, component } = this.props;
const {
analysis,
branchList,
@@ -259,7 +256,6 @@ export default class App extends React.PureComponent<Props, State> {
{generalSetting && overrideGeneralSetting !== undefined && (
<ProjectBaselineSelector
analysis={analysis}
- branch={branchLike}
branchList={branchList}
branchesEnabled={branchesEnabled}
component={component.key}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
index 0083374fb70..2b63146fa7f 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
@@ -27,7 +27,7 @@ import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';
interface Props {
analysis: string;
- branch?: string;
+ branch: string;
component: string;
onSelectAnalysis: (analysis: T.ParsedAnalysis) => void;
}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
index 4d133b25ee1..574ce1a048d 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
@@ -34,7 +34,6 @@ import BranchAnalysisList from './BranchAnalysisList';
export interface ProjectBaselineSelectorProps {
analysis?: string;
- branch: Branch;
branchList: Branch[];
branchesEnabled?: boolean;
component: string;
@@ -83,7 +82,6 @@ function branchToOption(b: Branch) {
export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) {
const {
analysis,
- branch,
branchList,
branchesEnabled,
component,
@@ -165,7 +163,7 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
{selected === 'SPECIFIC_ANALYSIS' && (
<BranchAnalysisList
analysis={analysis || ''}
- branch={branch.name}
+ branch="master"
component={component}
onSelectAnalysis={props.onSelectAnalysis}
/>
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
index 65cd8e22f08..f4c3fd6bc94 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
@@ -35,14 +35,8 @@ jest.mock('../../../../api/newCodePeriod', () => ({
setNewCodePeriod: jest.fn().mockResolvedValue({})
}));
-it('should render correctly', async () => {
- let wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-
- wrapper = shallowRender({ branchesEnabled: false });
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot('without branch support');
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
});
it('should initialize correctly', async () => {
@@ -106,7 +100,6 @@ it('should handle errors gracefully', async () => {
function shallowRender(props: Partial<App['props']> = {}) {
return shallow<App>(
<App
- branchLike={mockBranch()}
branchLikes={[mockMainBranch()]}
branchesEnabled={true}
canAdmin={true}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
index 294b63939ee..0e1cf612c5a 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
@@ -19,7 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
+import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import ProjectBaselineSelector, { ProjectBaselineSelectorProps } from '../ProjectBaselineSelector';
it('should render correctly', () => {
@@ -105,7 +105,6 @@ it('should disable the save button when date is invalid', () => {
function shallowRender(props: Partial<ProjectBaselineSelectorProps> = {}) {
return shallow(
<ProjectBaselineSelector
- branch={mockBranch()}
branchList={[mockMainBranch()]}
branchesEnabled={true}
component=""
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
index e642a0c6573..7b76b1ebb90 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -11,186 +11,7 @@ exports[`should render correctly 1`] = `
<AppHeader
canAdmin={true}
/>
- <div
- className="panel-white project-baseline"
- >
- <h2>
- project_baseline.default_setting
- </h2>
- <ProjectBaselineSelector
- analysis=""
- branch={
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": false,
- "name": "branch-6.7",
- }
- }
- branchList={
- Array [
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": true,
- "name": "master",
- },
- ]
- }
- branchesEnabled={true}
- component="my-project"
- currentSetting="PREVIOUS_VERSION"
- days="30"
- generalSetting={
- Object {
- "type": "PREVIOUS_VERSION",
- }
- }
- onCancel={[Function]}
- onSelectAnalysis={[Function]}
- onSelectDays={[Function]}
- onSelectReferenceBranch={[Function]}
- onSelectSetting={[Function]}
- onSubmit={[Function]}
- onToggleSpecificSetting={[Function]}
- overrideGeneralSetting={true}
- referenceBranch="master"
- saving={false}
- selected="PREVIOUS_VERSION"
- />
- <div
- className="spacer-top invisible"
- >
- <span
- className="text-success"
- >
- <AlertSuccessIcon
- className="spacer-right"
- />
- settings.state.saved
- </span>
- </div>
- <div
- className="huge-spacer-top branch-baseline-selector"
- >
- <hr />
- <h2>
- project_baseline.configure_branches
- </h2>
- <BranchList
- branchList={
- Array [
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": true,
- "name": "master",
- },
- ]
- }
- component={
- Object {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "qualifier": "TRK",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- }
- }
- inheritedSetting={
- Object {
- "type": "PREVIOUS_VERSION",
- "value": undefined,
- }
- }
- />
- </div>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`should render correctly: without branch support 1`] = `
-<Fragment>
- <Suggestions
- suggestions="project_baseline"
- />
- <div
- className="page page-limited"
- >
- <AppHeader
- canAdmin={true}
- />
- <div
- className="panel-white project-baseline"
- >
- <ProjectBaselineSelector
- analysis=""
- branch={
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": false,
- "name": "branch-6.7",
- }
- }
- branchList={
- Array [
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": true,
- "name": "master",
- },
- ]
- }
- branchesEnabled={false}
- component="my-project"
- currentSetting="PREVIOUS_VERSION"
- days="30"
- generalSetting={
- Object {
- "type": "PREVIOUS_VERSION",
- }
- }
- onCancel={[Function]}
- onSelectAnalysis={[Function]}
- onSelectDays={[Function]}
- onSelectReferenceBranch={[Function]}
- onSelectSetting={[Function]}
- onSubmit={[Function]}
- onToggleSpecificSetting={[Function]}
- overrideGeneralSetting={true}
- referenceBranch="master"
- saving={false}
- selected="PREVIOUS_VERSION"
- />
- <div
- className="spacer-top invisible"
- >
- <span
- className="text-success"
- >
- <AlertSuccessIcon
- className="spacer-right"
- />
- settings.state.saved
- </span>
- </div>
- </div>
+ <DeferredSpinner />
</div>
</Fragment>
`;