From: lukasz-jarocki-sonarsource <77498856+lukasz-jarocki-sonarsource@users.noreply.github.com> Date: Fri, 26 Feb 2021 08:29:39 +0000 (+0100) Subject: Revert SONAR-14478, SONAR-14462, SONAR-14461 X-Git-Tag: 8.8.0.42792~110 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=9cb17b6dbce261af578b7c5fe430fa340d4ff1ad;p=sonarqube.git 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. --- 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 = 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 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 { ); } + renderSonarCloudLinks() { + return ( + <> +
  • +
  • + + {translate('embed_docs.get_help')} + +
  • +
  • + {this.renderTitle(translate('embed_docs.stay_connected'))} +
  • + {this.renderIconLink( + 'https://twitter.com/sonarcloud', + 'embed-doc/twitter-icon.svg', + 'Twitter' + )} +
  • +
  • + {this.renderIconLink( + 'https://blog.sonarsource.com/product/SonarCloud', + 'sonarcloud-square-logo.svg', + translate('embed_docs.blog') + )} +
  • +
  • + +
  • + + ); + } + + renderSonarQubeLinks() { + return ( + <> +
  • +
  • + + {translate('embed_docs.get_help')} + +
  • +
  • + {this.renderTitle(translate('embed_docs.stay_connected'))} +
  • + {this.renderIconLink( + 'https://www.sonarqube.org/whats-new/?referrer=sonarqube', + 'embed-doc/sq-icon.svg', + translate('embed_docs.news') + )} +
  • +
  • + {this.renderIconLink( + 'https://twitter.com/SonarQube', + 'embed-doc/twitter-icon.svg', + 'Twitter' + )} +
  • + + ); + } + render() { return ( @@ -82,28 +148,7 @@ export default class EmbedDocsPopup extends React.PureComponent { {translate('api_documentation.page')} -
  • -
  • - - {translate('embed_docs.get_help')} - -
  • -
  • - {this.renderTitle(translate('embed_docs.stay_connected'))} -
  • - {this.renderIconLink( - 'https://www.sonarqube.org/whats-new/?referrer=sonarqube', - 'embed-doc/sq-icon.svg', - translate('embed_docs.news') - )} -
  • -
  • - {this.renderIconLink( - 'https://twitter.com/SonarQube', - 'embed-doc/twitter-icon.svg', - 'Twitter' - )} -
  • + {isSonarCloud() ? this.renderSonarCloudLinks() : this.renderSonarQubeLinks()}
    ); 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 { + 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 ( + +
    +
    +

    {translate('embed_docs.latest_blog')}

    + + + +
    +

    + {' '} + {' '} + +

    +
    + +
    + ); + } + + render() { + const link = 'https://blog.sonarsource.com/'; + const { loading, news } = this.state; + + if (loading) { + return this.renderPlaceholder(); + } + + if (!news) { + return null; + } + + return ( + +
    +
    +

    {translate('embed_docs.latest_blog')}

    + + {formattedDate => {formattedDate}} + +
    +

    {news.data.title}

    +
    + +
    + ); + } +} + +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(); + 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`] = ` + +
    +
    +

    + embed_docs.latest_blog +

    + + + +
    +

    + + + + + + + + + + + +

    +
    + +
    +`; + +exports[`should load the product news 2`] = ` + +
    +
    +

    + embed_docs.latest_blog +

    + + + +
    +

    + My Product News +

    +
    + +
    +`; 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 ( { 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) { 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 { + mounted = false; + + checkHasUnread = () => { + const { notificationsLastReadDate, lastNews } = this.props; + return ( + !notificationsLastReadDate || + differenceInSeconds(parseDate(lastNews.publicationDate), notificationsLastReadDate) > 0 + ); + }; + + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onClick(); + }; + + handleDismiss = (event: React.MouseEvent) => { + 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 && ( + <> +
  • +
    + {translate('new')} + {lastNews.notification} +
    +
  • +
  • + + + +
  • + + )} +
  • + + + +
  • + + ); + } +} 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 ( + +
    +
    +

    {header}

    + +
    +
    + {loading ? ( +
    + +
    + ) : ( + news.map((slice, index) => ( + + )) + )} +
    + {!loading && paging && paging.total > news.length && ( +
    +
    + + {translate('show_more')} + + {loadingMore && ( + + )} +
    +
    + )} +
    +
    + ); +} + +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 ( +
    +

    + +

    + {notification.features.map((feature, index) => ( + + ))} +
    + ); +} + +interface FeatureProps { + feature: PrismicFeatureNews['features'][0]; +} + +export function Feature({ feature }: FeatureProps) { + return ( +
    +
      + {feature.categories.map(category => ( +
    • + {category.name} +
    • + ))} +
    + {feature.description} + {feature.readMore && ( + + {translate('learn_more')} + + )} +
    + ); +} 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 = {}) { + 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( + + ); +} 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()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); + }); +}); + +describe('#Feature', () => { + it('should render correctly', () => { + expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); + }); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} 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`] = ` + +
  • +
    + + new + + + 10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration + +
    +
  • +
  • + + + +
  • +
  • + + + +
  • +
    +`; + +exports[`should render correctly if there are new features, but the user has opted out 1`] = ` + +
  • + + + +
  • +
    +`; + +exports[`should render correctly if there are no new unread features 1`] = ` + +
  • + + + +
  • +
    +`; 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`] = ` +
    +
      +
    • + BitBucket +
    • +
    + + BitBucket branch decoration + + + learn_more + +
    +`; + +exports[`#Feature should render correctly 2`] = ` +
    +
      +
    • + Java +
    • +
    • + Rules +
    • +
    + + 10 new Java rules + +
    +`; + +exports[`#Notification should render correctly 1`] = ` +
    +

    + +

    + +
    +`; + +exports[`#Notification should render correctly 2`] = ` +
    +

    + +

    + +
    +`; + +exports[`#NotificationSidebar should render correctly if there are new features 1`] = ` + +
    +
    +

    + embed_docs.whats_new +

    + +
    +
    +
    + +
    +
    +
    +
    +`; + +exports[`#NotificationSidebar should render correctly if there are new features 2`] = ` + +
    +
    +

    + embed_docs.whats_new +

    + +
    +
    + + +
    + +
    +
    +`; 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 { } 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 { }; 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 { {generalSetting && overrideGeneralSetting !== undefined && ( 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' && ( 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 = {}) { return shallow( { @@ -105,7 +105,6 @@ it('should disable the save button when date is invalid', () => { function shallowRender(props: Partial = {}) { return shallow( -
    -

    - project_baseline.default_setting -

    - -
    - - - settings.state.saved - -
    -
    -
    -

    - project_baseline.configure_branches -

    - -
    -
    - - -`; - -exports[`should render correctly: without branch support 1`] = ` - - -
    - -
    - -
    - - - settings.state.saved - -
    -
    +
    `; diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java index b208a2359d4..6dfbe796073 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java @@ -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 diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java index adaa253f690..53efb72218b 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java @@ -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) { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java index 4303fa222ee..c7654504d0b 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java @@ -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 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 branchChangedFiles(String targetBranchName, Path rootBaseDir) { diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MetadataPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MetadataPublisherTest.java index d38a149a668..a7bda1c9992 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MetadataPublisherTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MetadataPublisherTest.java @@ -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(); - } } diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java index 4a9a6121aa9..4efb0ddcb33 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java @@ -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++) { diff --git a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto index f406f2bde9d..b2deb7be2f5 100644 --- a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto +++ b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto @@ -60,8 +60,6 @@ message Metadata { map not_analyzed_files_by_language = 20; - string gitDefaultMainBranch = 21; - message QProfile { string key = 1; string name = 2;