]> source.dussan.org Git - sonarqube.git/commitdiff
Revert SONAR-14478, SONAR-14462, SONAR-14461
authorlukasz-jarocki-sonarsource <77498856+lukasz-jarocki-sonarsource@users.noreply.github.com>
Fri, 26 Feb 2021 08:29:39 +0000 (09:29 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 26 Feb 2021 20:07:39 +0000 (20:07 +0000)
* 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.

32 files changed:
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStep.java [deleted file]
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStepTest.java [deleted file]
server/sonar-docs/src/pages/analysis/branch-pr-analysis-overview.md
server/sonar-docs/src/pages/branches/overview.md
server/sonar-web/src/main/js/api/news.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/notifications/notifications.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MetadataPublisherTest.java
sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java
sonar-scanner-protocol/src/main/protobuf/scanner_report.proto

index f04fe03a2a999989e38cf191c631c22042303a98..bfca57e6c576baef499b2088a13d393ee445f27d 100644 (file)
@@ -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 (file)
index 1d70c9b..0000000
+++ /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 (file)
index ff18ae5..0000000
+++ /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);
-  }
-}
index 91cdb97fae9c61d8e1a7b3381104345147326ff0..8335bf63c5fd520c0944f022363769c71005e1f2 100644 (file)
@@ -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.
 
index c95f0e1e2f8520635bed13625a898b2ab2c7f233..83c2660114f9695acb5d48bcbbae37e0cb74119d 100644 (file)
@@ -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 (file)
index 0000000..3b4e004
--- /dev/null
@@ -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
+    }
+  }));
+}
index ab3f44270e77052a71df897fe964fa3bc32a0022..d5e145cbeeacd720db2e73adeb3dd8d17daa44b3 100644 (file)
@@ -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 (file)
index 0000000..62ed86e
--- /dev/null
@@ -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 (file)
index 0000000..a6605fb
--- /dev/null
@@ -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 (file)
index 0000000..2cd9101
--- /dev/null
@@ -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>
+`;
index cafece6895c2e70c6082a165218c3dbd1caa3383..72729ff47ce200402020badf098bba3730a628fc 100644 (file)
@@ -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', {
index ff00b6d76246c7b2eedecfdb08d7168b1c38be17..c0b34d01686fd267655f00752d81fea7a1aa0345 100644 (file)
@@ -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}
     />
index 5e1897ca5c10684f5d98bd4a9bd552496148d52f..9a956e3303cec7b161d2a8161df95d61463d6edd 100644 (file)
@@ -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 (file)
index 0000000..30d4957
--- /dev/null
@@ -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 (file)
index 0000000..9253643
--- /dev/null
@@ -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 (file)
index 0000000..334563b
--- /dev/null
@@ -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 (file)
index 0000000..f9b92ec
--- /dev/null
@@ -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 (file)
index 0000000..c748dbf
--- /dev/null
@@ -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 (file)
index 0000000..4936ee1
--- /dev/null
@@ -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 (file)
index 0000000..ef18020
--- /dev/null
@@ -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;
+}
index ed9efc5b52166e9a7a59fd5513ae1c85f6216c59..32a1c54100d255901727e1dfa1c1b7a08d6a4127 100644 (file)
@@ -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}
index 0083374fb70722f13145c92f74a8d0700748f051..2b63146fa7f60fa4fb2fe9442eac62fe8fc37030 100644 (file)
@@ -27,7 +27,7 @@ import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';
 
 interface Props {
   analysis: string;
-  branch?: string;
+  branch: string;
   component: string;
   onSelectAnalysis: (analysis: T.ParsedAnalysis) => void;
 }
index 4d133b25ee17662ba13f66b0645065a33c6703e1..574ce1a048d1a410a39929a8863314ab46ec2aee 100644 (file)
@@ -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}
           />
index 65cd8e22f0833190929cf9d72eeb272a21014799..f4c3fd6bc94d66b4fe05cb54ffad066fd90cc125 100644 (file)
@@ -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}
index 294b63939ee9bf4e4869098d906f94c93df27482..0e1cf612c5a9762cd77af4d7eeb8931a65827c64 100644 (file)
@@ -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=""
index e642a0c6573118e572d415404bc90a00082722f7..7b76b1ebb90016e2d8a33017e08a203a74d0c08e 100644 (file)
@@ -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>
 `;
index b208a2359d43a34854e67e63fd26ec2f09608438..6dfbe7960736c07da0e19256876bc20658798f1f 100644 (file)
@@ -67,17 +67,6 @@ public abstract class ScmProvider {
     return null;
   }
 
-  /**
-   * Return the main branch name.
-   *
-   * @return null if the SCM provider was not able to find the main branch.
-   * @since 8.8
-   */
-  @CheckForNull
-  public String getMainBranch(Path rootBaseDir) {
-    return null;
-  }
-
   /**
    * Return a map between paths given as argument and the corresponding line numbers which are new compared to the provided target branch.
    * If nothing is returned for a file, the scanner will consider that the provider was unable to determine changes for that file and it will
index adaa253f690c3e5309517f8c64fdf907cb948504..53efb72218ba6f8a3bd91f4948e5dcc3d253c991 100644 (file)
@@ -61,9 +61,6 @@ public class MetadataPublisher implements ReportPublisherStep {
   private final InputComponentStore componentStore;
   private final ScmConfiguration scmConfiguration;
 
-  private ScmProvider scmProvider;
-  private Path projectBasedir;
-
   public MetadataPublisher(ProjectInfo projectInfo, InputModuleHierarchy moduleHierarchy, QualityProfiles qProfiles,
     CpdSettings cpdSettings, ScannerPluginRepository pluginRepository, BranchConfiguration branchConfiguration,
     ScmRevision scmRevision, ForkDateSupplier forkDateSupplier, InputComponentStore componentStore, ScmConfiguration scmConfiguration) {
@@ -112,28 +109,11 @@ public class MetadataPublisher implements ReportPublisherStep {
         .setUpdatedAt(pluginEntry.getValue().getUpdatedAt()).build());
     }
 
-    scmProvider = scmConfiguration.provider();
-    projectBasedir = moduleHierarchy.root().getBaseDir();
-
     addModulesRelativePaths(builder);
-    addMainBranch(builder);
 
     writer.writeMetadata(builder.build());
   }
 
-  private void addMainBranch(ScannerReport.Metadata.Builder builder) {
-    if (scmProvider == null) {
-      return;
-    }
-    String mainBranch = scmProvider.getMainBranch(projectBasedir);
-    if (mainBranch != null && !mainBranch.isEmpty()) {
-      LOG.debug("The main branch for '{}' is '{}'", projectBasedir.toString(), mainBranch);
-      builder.setGitDefaultMainBranch(mainBranch);
-    } else {
-      LOG.debug("The main branch for '{}' has not been found", projectBasedir.toString());
-    }
-  }
-
   private void addForkPoint(ScannerReport.Metadata.Builder builder) {
     Instant date = forkDateSupplier.get();
     if (date != null) {
@@ -154,10 +134,12 @@ public class MetadataPublisher implements ReportPublisherStep {
       }
     }
 
+    ScmProvider scmProvider = scmConfiguration.provider();
     if (scmProvider == null) {
       return;
     }
 
+    Path projectBasedir = moduleHierarchy.root().getBaseDir();
     try {
       builder.setRelativePathFromScmRoot(toSonarQubePath(scmProvider.relativePathFromScmRoot(projectBasedir)));
     } catch (UnsupportedOperationException e) {
index 4303fa222ee44586ad66e1bfd264358ab8aa0bc0..c7654504d0bc7e9187c912871e16f890f9c4f320 100644 (file)
@@ -97,21 +97,6 @@ public class GitScmProvider extends ScmProvider {
     return this.jgitBlameCommand;
   }
 
-  @CheckForNull
-  @Override
-  public String getMainBranch(Path rootBaseDir) {
-    try (Repository repo = buildRepo(rootBaseDir)) {
-      Set<String> branches = repo.getConfig().getSubsections("branch");
-      if(!branches.isEmpty()) {
-        // .git/config file will have the default branch at the time of cloning as its first branch
-        return branches.iterator().next();
-      }
-    } catch (IOException e) {
-      LOG.debug("Couldn't build a repo in order to retrieve the default branch name", e);
-    }
-    return null;
-  }
-
   @CheckForNull
   @Override
   public Set<Path> branchChangedFiles(String targetBranchName, Path rootBaseDir) {
index d38a149a668ce6c238653bfcd4e08c075ecb0b0e..a7bda1c999253524117550c621feb561b4e90ee6 100644 (file)
@@ -286,49 +286,4 @@ public class MetadataPublisherTest {
     ScannerReport.Metadata metadata = reader.readMetadata();
     assertThat(metadata.getRelativePathFromScmRoot()).isEmpty();
   }
-
-  @Test
-  public void addMainBranch_givenDefaultMainBranchSet_writeItToMetadata() throws IOException {
-    ScmProvider scmProvider = mock(ScmProvider.class);
-    when(scmProvider.getMainBranch(any())).thenReturn("Main");
-    when(scmProvider.relativePathFromScmRoot(any())).thenReturn(mock(Path.class));
-    when(scmConfiguration.provider()).thenReturn(scmProvider);
-
-    File outputDir = temp.newFolder();
-    underTest.publish(new ScannerReportWriter(outputDir));
-
-    ScannerReportReader reader = new ScannerReportReader(outputDir);
-    ScannerReport.Metadata metadata = reader.readMetadata();
-    assertThat(metadata.getGitDefaultMainBranch()).isEqualTo("Main");
-  }
-
-  @Test
-  public void addMainBranch_givenEmptyDefaultMainBranchSet_emptyDefaultMainBranchInMetadata() throws IOException {
-    ScmProvider scmProvider = mock(ScmProvider.class);
-    when(scmProvider.getMainBranch(any())).thenReturn("");
-    when(scmProvider.relativePathFromScmRoot(any())).thenReturn(mock(Path.class));
-    when(scmConfiguration.provider()).thenReturn(scmProvider);
-
-    File outputDir = temp.newFolder();
-    underTest.publish(new ScannerReportWriter(outputDir));
-
-    ScannerReportReader reader = new ScannerReportReader(outputDir);
-    ScannerReport.Metadata metadata = reader.readMetadata();
-    assertThat(metadata.getGitDefaultMainBranch()).isEmpty();
-  }
-
-  @Test
-  public void addMainBranch_givenNullMainBranchSet_emptyDefaultMainBranchInMetadata() throws IOException {
-    ScmProvider scmProvider = mock(ScmProvider.class);
-    when(scmProvider.getMainBranch(any())).thenReturn(null);
-    when(scmProvider.relativePathFromScmRoot(any())).thenReturn(mock(Path.class));
-    when(scmConfiguration.provider()).thenReturn(scmProvider);
-
-    File outputDir = temp.newFolder();
-    underTest.publish(new ScannerReportWriter(outputDir));
-
-    ScannerReportReader reader = new ScannerReportReader(outputDir);
-    ScannerReport.Metadata metadata = reader.readMetadata();
-    assertThat(metadata.getGitDefaultMainBranch()).isEmpty();
-  }
 }
index 4a9a6121aa978e0abda09e777c36e5087d18ca46..4efb0ddcb3335392719c218105969d11d6fd559e 100644 (file)
@@ -39,19 +39,15 @@ import java.util.Random;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.concurrent.atomic.AtomicInteger;
-
-import org.eclipse.jgit.api.CreateBranchCommand;
 import org.eclipse.jgit.api.DiffCommand;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
-import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
 import org.junit.Before;
 import org.junit.Rule;
@@ -736,132 +732,6 @@ public class GitScmProviderTest {
     assertThat(provider.revisionId(projectDir)).isNull();
   }
 
-  @Test
-  public void getMainBranch_givenRepoWithOneBranchCalledMain_returnMainBranchCalledMain() throws Exception {
-    //given
-    worktree = temp.newFolder().toPath();
-    Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
-    repo.create();
-    git = new Git(repo);
-
-    addBranchInConfig("Main");
-
-    Path projectDir = worktree.resolve("project");
-    Files.createDirectory(projectDir);
-
-    GitScmProvider provider = newGitScmProvider();
-
-    //when
-    String mainBranch = provider.getMainBranch(projectDir);
-
-    //then
-    assertThat(mainBranch).isEqualTo("Main");
-  }
-
-  @Test
-  public void getMainBranch_givenRepoWithTwoBranches_returnFirstBranch() throws Exception {
-    //given
-    worktree = temp.newFolder().toPath();
-    Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
-    repo.create();
-    git = new Git(repo);
-
-    addBranchInConfig("First");
-    addBranchInConfig("Second");
-
-    Path projectDir = worktree.resolve("project");
-    Files.createDirectory(projectDir);
-
-    GitScmProvider provider = newGitScmProvider();
-
-    //when
-    String mainBranch = provider.getMainBranch(projectDir);
-
-    //then
-    assertThat(mainBranch).isEqualTo("First");
-  }
-
-  @Test
-  public void getMainBranch_givenNoBranches_dontThrowException() throws Exception {
-    //given
-    worktree = temp.newFolder().toPath();
-    Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
-    repo.create();
-    git = new Git(repo);
-
-    Path projectDir = worktree.resolve("project");
-    Files.createDirectory(projectDir);
-
-    GitScmProvider provider = newGitScmProvider();
-
-    //when
-    String mainBranch = provider.getMainBranch(projectDir);
-
-    //then no exception
-    assertThat(mainBranch).isNullOrEmpty();
-  }
-
-  @Test
-  public void getMainBranch_givenRepositoryNotFoundExceptionWhenBuildingRepo_returnNull() throws Exception {
-    //given
-
-    worktree = temp.newFolder().toPath();
-    Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
-    repo.create();
-    git = new Git(repo);
-    repo.getObjectDatabase().close(); //This is here to force RepositoryBuilder to throw subclass of IOException
-
-    Path projectDir = worktree.resolve("project");
-    Files.createDirectory(projectDir);
-
-    GitScmProvider provider = newGitScmProvider();
-
-    //when
-    String mainBranch = provider.getMainBranch(projectDir);
-
-    //then no exception
-    assertThat(mainBranch).isNullOrEmpty();
-  }
-
-  @Test
-  public void getMainBranch_givenIOExceptionWhenBuildingRepo_returnNull() throws Exception {
-    //given
-
-    worktree = temp.newFolder().toPath();
-    Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
-    repo.create();
-    git = new Git(repo);
-
-    Path projectDir = worktree.resolve("project");
-    Files.createDirectory(projectDir);
-
-    GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) {
-      @Override
-      Repository buildRepo(Path basedir) throws IOException {
-        throw new IOException();
-      }
-    };
-
-    //when
-    String mainBranch = provider.getMainBranch(projectDir);
-
-    //then no exception
-    assertThat(mainBranch).isNullOrEmpty();
-  }
-
-  /**
-   * Normally after cloning the repository we would have at least one
-   * branch it git config. This method adds these branches without
-   * cloning any repository (because unit tests ought to be fast)
-   */
-  private void addBranchInConfig(String ... branches) throws IOException {
-    for(String branch : branches) {
-      git.getRepository().getConfig().setStringList("branch", branch, "remote", Arrays.asList("origin"));
-      git.getRepository().getConfig().setStringList("branch", branch, "merge", Arrays.asList("refs/head/" + branch));
-    }
-    git.getRepository().getConfig().save();
-  }
-
   private String randomizedContent(String prefix, int numLines) {
     StringBuilder sb = new StringBuilder();
     for (int line = 0; line < numLines; line++) {
index f406f2bde9d58a31372685577b274e9d2447f020..b2deb7be2f5e522a14038f848244129181e583e1 100644 (file)
@@ -60,8 +60,6 @@ message Metadata {
 
   map<string, int32> not_analyzed_files_by_language = 20;
 
-  string gitDefaultMainBranch = 21;
-
   message QProfile {
     string key = 1;
     string name = 2;