aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorlukasz-jarocki-sonarsource <77498856+lukasz-jarocki-sonarsource@users.noreply.github.com>2021-02-26 09:29:39 +0100
committersonartech <sonartech@sonarsource.com>2021-02-26 20:07:39 +0000
commit9cb17b6dbce261af578b7c5fe430fa340d4ff1ad (patch)
treedecb117810be86812b2849f8adb388de5a97919d /server/sonar-web
parent26ca7559fab8b6a0379468a0a5b5f7d83c1baaa9 (diff)
downloadsonarqube-9cb17b6dbce261af578b7c5fe430fa340d4ff1ad.tar.gz
sonarqube-9cb17b6dbce261af578b7c5fe430fa340d4ff1ad.zip
Revert SONAR-14478, SONAR-14462, SONAR-14461
* Revert "SONAR-14478 - Main Branch Documentation" This reverts commit 59eae7cf3f2e611e162a4e0122ae5846b10a45b1. * Revert "SONAR-14462 Do not display the branch name until the main branch is analyzed for the first time" This reverts commit 20f7319c06affdae62d39d1bad002f16504465a2. * Revert "SONAR-14461 main branch detection" This reverts commit c04baa1e8e3b492953d66a6bc4111c01f3ee3069. * Revert "SONAR-14461 Remove hardcoded usage of 'master'" This reverts commit 32eefaf2d36af375af280cc3ba664fd71e0f6afd. * Revert "SONAR-14461 save the default main branch when needed" This reverts commit 879a4be2afc570b2248fb4d639f42f913215805b.
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/news.ts139
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx89
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx135
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap97
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx5
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx19
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap3
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx93
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx134
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx67
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx119
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap82
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap269
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/notifications.css157
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap181
21 files changed, 1417 insertions, 250 deletions
diff --git a/server/sonar-web/src/main/js/api/news.ts b/server/sonar-web/src/main/js/api/news.ts
new file mode 100644
index 00000000000..3b4e00488d0
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/news.ts
@@ -0,0 +1,139 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { getCorsJSON } from 'sonar-ui-common/helpers/request';
+
+interface PrismicRef {
+ id: string;
+ ref: string;
+}
+
+export interface PrismicNews {
+ data: { title: string };
+ last_publication_date: string;
+ uid: string;
+}
+
+interface PrismicResponse {
+ page: number;
+ results: PrismicResult[];
+ results_per_page: number;
+ total_results_size: number;
+}
+
+interface PrismicResult {
+ data: {
+ notification: string;
+ publication_date: string;
+ body: PrismicResultFeature[];
+ };
+}
+
+interface PrismicResultFeature {
+ items: Array<{
+ category: {
+ data: {
+ color: string;
+ name: string;
+ };
+ };
+ }>;
+ primary: {
+ description: string;
+ read_more_link: {
+ url?: string;
+ };
+ };
+}
+
+export interface PrismicFeatureNews {
+ notification: string;
+ publicationDate: string;
+ features: Array<{
+ categories: Array<{
+ color: string;
+ name: string;
+ }>;
+ description: string;
+ readMore?: string;
+ }>;
+}
+
+const PRISMIC_API_URL = 'https://sonarsource.cdn.prismic.io/api/v2';
+
+export function fetchPrismicRefs() {
+ return getCorsJSON(PRISMIC_API_URL).then((response: { refs: PrismicRef[] }) => {
+ const master = response && response.refs.find(ref => ref.id === 'master');
+ if (!master) {
+ return Promise.reject('No master ref found');
+ }
+ return master;
+ });
+}
+
+export function fetchPrismicNews(data: {
+ accessToken: string;
+ ps?: number;
+ ref: string;
+ tag?: string;
+}) {
+ const q = ['[[at(document.type, "blog_sonarsource_post")]]'];
+ if (data.tag) {
+ q.push(`[[at(document.tags,["${data.tag}"])]]`);
+ }
+ return getCorsJSON(PRISMIC_API_URL + '/documents/search', {
+ access_token: data.accessToken,
+ orderings: '[document.first_publication_date desc]',
+ pageSize: data.ps || 1,
+ q,
+ ref: data.ref
+ }).then(({ results }: { results: PrismicNews[] }) => results);
+}
+
+export function fetchPrismicFeatureNews(data: {
+ accessToken: string;
+ p?: number;
+ ps?: number;
+ ref: string;
+}): Promise<{ news: PrismicFeatureNews[]; paging: T.Paging }> {
+ return getCorsJSON(PRISMIC_API_URL + '/documents/search', {
+ access_token: data.accessToken,
+ fetchLinks: 'sc_category.color,sc_category.name',
+ orderings: '[my.sc_product_news.publication_date desc]',
+ page: data.p || 1,
+ pageSize: data.ps || 1,
+ q: ['[[at(document.type, "sc_product_news")]]'],
+ ref: data.ref
+ }).then(({ page, results, results_per_page, total_results_size }: PrismicResponse) => ({
+ news: results.map(result => ({
+ notification: result.data.notification,
+ publicationDate: result.data.publication_date,
+ features: result.data.body.map(feature => ({
+ categories: feature.items.map(item => item.category.data).filter(Boolean),
+ description: feature.primary.description,
+ readMore: feature.primary.read_more_link.url
+ }))
+ })),
+ paging: {
+ pageIndex: page,
+ pageSize: results_per_page,
+ total: total_results_size
+ }
+ }));
+}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
index ab3f44270e7..d5e145cbeea 100644
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
@@ -22,6 +22,8 @@ import { Link } from 'react-router';
import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
+import { isSonarCloud } from '../../../helpers/system';
+import ProductNewsMenuItem from './ProductNewsMenuItem';
import { SuggestionsContext } from './SuggestionsContext';
interface Props {
@@ -67,6 +69,70 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
);
}
+ renderSonarCloudLinks() {
+ return (
+ <>
+ <li className="divider" />
+ <li>
+ <a
+ href="https://community.sonarsource.com/c/help/sc"
+ rel="noopener noreferrer"
+ target="_blank">
+ {translate('embed_docs.get_help')}
+ </a>
+ </li>
+ <li className="divider" />
+ {this.renderTitle(translate('embed_docs.stay_connected'))}
+ <li>
+ {this.renderIconLink(
+ 'https://twitter.com/sonarcloud',
+ 'embed-doc/twitter-icon.svg',
+ 'Twitter'
+ )}
+ </li>
+ <li>
+ {this.renderIconLink(
+ 'https://blog.sonarsource.com/product/SonarCloud',
+ 'sonarcloud-square-logo.svg',
+ translate('embed_docs.blog')
+ )}
+ </li>
+ <li>
+ <ProductNewsMenuItem tag="SonarCloud" />
+ </li>
+ </>
+ );
+ }
+
+ renderSonarQubeLinks() {
+ return (
+ <>
+ <li className="divider" />
+ <li>
+ <a href="https://community.sonarsource.com/" rel="noopener noreferrer" target="_blank">
+ {translate('embed_docs.get_help')}
+ </a>
+ </li>
+ <li className="divider" />
+ {this.renderTitle(translate('embed_docs.stay_connected'))}
+ <li>
+ {this.renderIconLink(
+ 'https://www.sonarqube.org/whats-new/?referrer=sonarqube',
+ 'embed-doc/sq-icon.svg',
+ translate('embed_docs.news')
+ )}
+ </li>
+ <li>
+ {this.renderIconLink(
+ 'https://twitter.com/SonarQube',
+ 'embed-doc/twitter-icon.svg',
+ 'Twitter'
+ )}
+ </li>
+ </>
+ );
+ }
+
render() {
return (
<DropdownOverlay>
@@ -82,28 +148,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
{translate('api_documentation.page')}
</Link>
</li>
- <li className="divider" />
- <li>
- <a href="https://community.sonarsource.com/" rel="noopener noreferrer" target="_blank">
- {translate('embed_docs.get_help')}
- </a>
- </li>
- <li className="divider" />
- {this.renderTitle(translate('embed_docs.stay_connected'))}
- <li>
- {this.renderIconLink(
- 'https://www.sonarqube.org/whats-new/?referrer=sonarqube',
- 'embed-doc/sq-icon.svg',
- translate('embed_docs.news')
- )}
- </li>
- <li>
- {this.renderIconLink(
- 'https://twitter.com/SonarQube',
- 'embed-doc/twitter-icon.svg',
- 'Twitter'
- )}
- </li>
+ {isSonarCloud() ? this.renderSonarCloudLinks() : this.renderSonarQubeLinks()}
</ul>
</DropdownOverlay>
);
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx
new file mode 100644
index 00000000000..62ed86eae5f
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { connect } from 'react-redux';
+import ChevronRightIcon from 'sonar-ui-common/components/icons/ChevronRightIcon';
+import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { fetchPrismicNews, fetchPrismicRefs, PrismicNews } from '../../../api/news';
+import PlaceholderBar from '../../../components/ui/PlaceholderBar';
+import { getGlobalSettingValue, Store } from '../../../store/rootReducer';
+
+interface OwnProps {
+ tag?: string;
+}
+
+interface StateProps {
+ accessToken?: string;
+}
+
+type Props = OwnProps & StateProps;
+
+interface State {
+ loading: boolean;
+ news?: PrismicNews;
+}
+
+export class ProductNewsMenuItem extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchProductNews();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchProductNews = () => {
+ const { accessToken, tag } = this.props;
+ if (accessToken) {
+ this.setState({ loading: true });
+ fetchPrismicRefs()
+ .then(({ ref }) => fetchPrismicNews({ accessToken, ref, tag }))
+ .then(
+ news => {
+ if (this.mounted) {
+ this.setState({ news: news[0], loading: false });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ }
+ };
+
+ renderPlaceholder() {
+ return (
+ <a className="rich-item new-loading">
+ <div className="flex-1">
+ <div className="display-inline-flex-center">
+ <h4>{translate('embed_docs.latest_blog')}</h4>
+ <span className="note spacer-left">
+ <PlaceholderBar color="#aaa" width={60} />
+ </span>
+ </div>
+ <p className="little-spacer-bottom">
+ <PlaceholderBar color="#aaa" width={84} /> <PlaceholderBar color="#aaa" width={48} />{' '}
+ <PlaceholderBar color="#aaa" width={24} /> <PlaceholderBar color="#aaa" width={72} />{' '}
+ <PlaceholderBar color="#aaa" width={24} /> <PlaceholderBar color="#aaa" width={48} />
+ </p>
+ </div>
+ <ChevronRightIcon className="flex-0" />
+ </a>
+ );
+ }
+
+ render() {
+ const link = 'https://blog.sonarsource.com/';
+ const { loading, news } = this.state;
+
+ if (loading) {
+ return this.renderPlaceholder();
+ }
+
+ if (!news) {
+ return null;
+ }
+
+ return (
+ <a className="rich-item" href={link + news.uid} rel="noopener noreferrer" target="_blank">
+ <div className="flex-1">
+ <div className="display-inline-flex-center">
+ <h4>{translate('embed_docs.latest_blog')}</h4>
+ <DateFormatter date={news.last_publication_date}>
+ {formattedDate => <span className="note spacer-left">{formattedDate}</span>}
+ </DateFormatter>
+ </div>
+ <p className="little-spacer-bottom">{news.data.title}</p>
+ </div>
+ <ChevronRightIcon className="flex-0" />
+ </a>
+ );
+ }
+}
+
+const mapStateToProps = (state: Store): StateProps => {
+ const accessToken = getGlobalSettingValue(state, 'sonar.prismic.accessToken');
+ return {
+ accessToken: accessToken && accessToken.value
+ };
+};
+
+export default connect(mapStateToProps)(ProductNewsMenuItem);
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx
new file mode 100644
index 00000000000..a6605fb1d6a
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { fetchPrismicNews, fetchPrismicRefs } from '../../../../api/news';
+import { ProductNewsMenuItem } from '../ProductNewsMenuItem';
+
+jest.mock('../../../../api/news', () => ({
+ fetchPrismicRefs: jest.fn().mockResolvedValue({ id: 'master', ref: 'master-ref' }),
+ fetchPrismicNews: jest.fn().mockResolvedValue([
+ {
+ data: { title: 'My Product News' },
+ last_publication_date: '2018-04-06T12:07:19+0000',
+ uid: 'my-product-news'
+ }
+ ])
+}));
+
+it('should load the product news', async () => {
+ const wrapper = shallow(<ProductNewsMenuItem accessToken="token" tag="SonarCloud" />);
+ expect(wrapper).toMatchSnapshot();
+ await waitAndUpdate(wrapper);
+ expect(fetchPrismicRefs).toHaveBeenCalled();
+ expect(fetchPrismicNews).toHaveBeenCalledWith({
+ accessToken: 'token',
+ ref: 'master-ref',
+ tag: 'SonarCloud'
+ });
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap
new file mode 100644
index 00000000000..2cd910101ba
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap
@@ -0,0 +1,97 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should load the product news 1`] = `
+<a
+ className="rich-item new-loading"
+>
+ <div
+ className="flex-1"
+ >
+ <div
+ className="display-inline-flex-center"
+ >
+ <h4>
+ embed_docs.latest_blog
+ </h4>
+ <span
+ className="note spacer-left"
+ >
+ <PlaceholderBar
+ color="#aaa"
+ width={60}
+ />
+ </span>
+ </div>
+ <p
+ className="little-spacer-bottom"
+ >
+ <PlaceholderBar
+ color="#aaa"
+ width={84}
+ />
+
+ <PlaceholderBar
+ color="#aaa"
+ width={48}
+ />
+
+ <PlaceholderBar
+ color="#aaa"
+ width={24}
+ />
+
+ <PlaceholderBar
+ color="#aaa"
+ width={72}
+ />
+
+ <PlaceholderBar
+ color="#aaa"
+ width={24}
+ />
+
+ <PlaceholderBar
+ color="#aaa"
+ width={48}
+ />
+ </p>
+ </div>
+ <ChevronRightIcon
+ className="flex-0"
+ />
+</a>
+`;
+
+exports[`should load the product news 2`] = `
+<a
+ className="rich-item"
+ href="https://blog.sonarsource.com/my-product-news"
+ rel="noopener noreferrer"
+ target="_blank"
+>
+ <div
+ className="flex-1"
+ >
+ <div
+ className="display-inline-flex-center"
+ >
+ <h4>
+ embed_docs.latest_blog
+ </h4>
+ <DateFormatter
+ date="2018-04-06T12:07:19+0000"
+ >
+ <Component />
+ </DateFormatter>
+ </div>
+ <p
+ className="little-spacer-bottom"
+ >
+ My Product News
+ </p>
+ </div>
+ <ChevronRightIcon
+ className="flex-0"
+ />
+</a>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
index cafece6895c..72729ff47ce 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
@@ -57,11 +57,6 @@ export function BranchLikeNavigation(props: BranchLikeNavigationProps) {
/>
);
- // Main branch hasn't been analyzed yet && (CE || (DE+ && only one branch))
- if (!component.analysisDate && (!branchesEnabled || !hasManyBranches)) {
- return null;
- }
-
return (
<span
className={classNames('big-spacer-left flex-0 branch-like-navigation-toggler-container', {
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
index ff00b6d7624..c0b34d01686 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
@@ -30,23 +30,6 @@ it('should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
-it('should not render', () => {
- // CE && main branch not analyzed yet
- const wrapper = shallowRender({
- appState: mockAppState({ branchesEnabled: false }),
- component: mockComponent({ analysisDate: undefined })
- });
- expect(wrapper.type()).toBeNull();
-
- // DE+ && main branch not analyzed yet && no other branches
- const wrapper1 = shallowRender({
- appState: mockAppState({ branchesEnabled: true }),
- component: mockComponent({ analysisDate: undefined }),
- branchLikes: []
- });
- expect(wrapper1.type()).toBeNull();
-});
-
it('should render the menu trigger if branches are enabled', () => {
const wrapper = shallowRender({ appState: mockAppState({ branchesEnabled: true }) });
expect(wrapper).toMatchSnapshot();
@@ -84,7 +67,7 @@ function shallowRender(props?: Partial<BranchLikeNavigationProps>) {
<BranchLikeNavigation
appState={mockAppState()}
branchLikes={branchLikes}
- component={mockComponent({ analysisDate: '2021-01-01 01:01:01' })}
+ component={mockComponent()}
currentBranchLike={branchLikes[0]}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
index 5e1897ca5c1..9a956e3303c 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
@@ -8,7 +8,6 @@ exports[`should render correctly 1`] = `
branchesEnabled={false}
component={
Object {
- "analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
@@ -118,7 +117,6 @@ exports[`should render the menu trigger if branches are enabled 1`] = `
}
component={
Object {
- "analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
@@ -160,7 +158,6 @@ exports[`should render the menu trigger if branches are enabled 1`] = `
branchesEnabled={true}
component={
Object {
- "analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
diff --git a/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx b/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
new file mode 100644
index 00000000000..30d49578d50
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as differenceInSeconds from 'date-fns/difference_in_seconds';
+import * as React from 'react';
+import ClearIcon from 'sonar-ui-common/components/icons/ClearIcon';
+import NotificationIcon from 'sonar-ui-common/components/icons/NotificationIcon';
+import { parseDate } from 'sonar-ui-common/helpers/dates';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { PrismicFeatureNews } from '../../../api/news';
+import './notifications.css';
+
+interface Props {
+ lastNews: PrismicFeatureNews;
+ notificationsLastReadDate?: Date;
+ notificationsOptOut?: boolean;
+ onClick: () => void;
+ setCurrentUserSetting: (setting: T.CurrentUserSetting) => void;
+}
+
+export default class NavLatestNotification extends React.PureComponent<Props> {
+ mounted = false;
+
+ checkHasUnread = () => {
+ const { notificationsLastReadDate, lastNews } = this.props;
+ return (
+ !notificationsLastReadDate ||
+ differenceInSeconds(parseDate(lastNews.publicationDate), notificationsLastReadDate) > 0
+ );
+ };
+
+ handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onClick();
+ };
+
+ handleDismiss = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.props.setCurrentUserSetting({
+ key: 'notifications.readDate',
+ value: Date.now().toString()
+ });
+ };
+
+ render() {
+ const { notificationsOptOut, lastNews } = this.props;
+ const hasUnread = this.checkHasUnread();
+ const showNotifications = Boolean(!notificationsOptOut && lastNews && hasUnread);
+ return (
+ <>
+ {showNotifications && (
+ <>
+ <li className="navbar-latest-notification" onClick={this.props.onClick}>
+ <div className="navbar-latest-notification-wrapper">
+ <span className="badge badge-info">{translate('new')}</span>
+ <span className="label">{lastNews.notification}</span>
+ </div>
+ </li>
+ <li className="navbar-latest-notification-dismiss">
+ <a className="navbar-icon" href="#" onClick={this.handleDismiss}>
+ <ClearIcon size={12} thin={true} />
+ </a>
+ </li>
+ </>
+ )}
+ <li>
+ <a className="navbar-icon" href="#" onClick={this.handleClick}>
+ <NotificationIcon hasUnread={hasUnread && !notificationsOptOut} />
+ </a>
+ </li>
+ </>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx b/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx
new file mode 100644
index 00000000000..92536433eb9
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx
@@ -0,0 +1,134 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as differenceInSeconds from 'date-fns/difference_in_seconds';
+import * as React from 'react';
+import { ClearButton } from 'sonar-ui-common/components/controls/buttons';
+import Modal from 'sonar-ui-common/components/controls/Modal';
+import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { PrismicFeatureNews } from '../../../api/news';
+
+export interface Props {
+ fetchMoreFeatureNews: () => void;
+ loading: boolean;
+ loadingMore: boolean;
+ news: PrismicFeatureNews[];
+ onClose: () => void;
+ notificationsLastReadDate?: Date;
+ paging?: T.Paging;
+}
+
+export default function NotificationsSidebar(props: Props) {
+ const { loading, loadingMore, news, notificationsLastReadDate, paging } = props;
+ const header = translate('embed_docs.whats_new');
+ return (
+ <Modal contentLabel={header} onRequestClose={props.onClose}>
+ <div className="notifications-sidebar">
+ <div className="notifications-sidebar-top">
+ <h3>{header}</h3>
+ <ClearButton
+ className="button-tiny"
+ iconProps={{ size: 12, thin: true }}
+ onClick={props.onClose}
+ />
+ </div>
+ <div className="notifications-sidebar-content">
+ {loading ? (
+ <div className="text-center">
+ <DeferredSpinner className="big-spacer-top" timeout={200} />
+ </div>
+ ) : (
+ news.map((slice, index) => (
+ <Notification
+ key={slice.publicationDate}
+ notification={slice}
+ unread={isUnread(index, slice.publicationDate, notificationsLastReadDate)}
+ />
+ ))
+ )}
+ </div>
+ {!loading && paging && paging.total > news.length && (
+ <div className="notifications-sidebar-footer">
+ <div className="spacer-top note text-center">
+ <a className="spacer-left" href="#" onClick={props.fetchMoreFeatureNews}>
+ {translate('show_more')}
+ </a>
+ {loadingMore && (
+ <DeferredSpinner className="text-bottom spacer-left position-absolute" />
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ </Modal>
+ );
+}
+
+export function isUnread(index: number, notificationDate: string, lastReadDate?: Date) {
+ return !lastReadDate ? index < 1 : differenceInSeconds(notificationDate, lastReadDate) > 0;
+}
+
+interface NotificationProps {
+ notification: PrismicFeatureNews;
+ unread: boolean;
+}
+
+export function Notification({ notification, unread }: NotificationProps) {
+ return (
+ <div className={classNames('notifications-sidebar-slice', { unread })}>
+ <h4>
+ <DateFormatter date={notification.publicationDate} long={false} />
+ </h4>
+ {notification.features.map((feature, index) => (
+ <Feature feature={feature} key={index} />
+ ))}
+ </div>
+ );
+}
+
+interface FeatureProps {
+ feature: PrismicFeatureNews['features'][0];
+}
+
+export function Feature({ feature }: FeatureProps) {
+ return (
+ <div className="feature">
+ <ul className="categories spacer-bottom">
+ {feature.categories.map(category => (
+ <li key={category.name} style={{ backgroundColor: category.color }}>
+ {category.name}
+ </li>
+ ))}
+ </ul>
+ <span>{feature.description}</span>
+ {feature.readMore && (
+ <a
+ className="learn-more"
+ href={feature.readMore}
+ rel="noopener noreferrer nofollow"
+ target="_blank">
+ {translate('learn_more')}
+ </a>
+ )}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx b/server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx
new file mode 100644
index 00000000000..334563bdced
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { parseDate } from 'sonar-ui-common/helpers/dates';
+import { PrismicFeatureNews } from '../../../../api/news';
+import NavLatestNotification from '../NavLatestNotification';
+
+it('should render correctly if there are new features, and the user has not opted out', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('.navbar-latest-notification')).toHaveLength(1);
+});
+
+it('should render correctly if there are new features, but the user has opted out', () => {
+ const wrapper = shallowRender({ notificationsOptOut: true });
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('.navbar-latest-notification')).toHaveLength(0);
+});
+
+it('should render correctly if there are no new unread features', () => {
+ const wrapper = shallowRender({
+ notificationsLastReadDate: parseDate('2018-12-31T12:07:19+0000')
+ });
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('.navbar-latest-notification')).toHaveLength(0);
+});
+
+function shallowRender(props: Partial<NavLatestNotification['props']> = {}) {
+ const lastNews: PrismicFeatureNews = {
+ notification: '10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration',
+ publicationDate: '2018-04-06',
+ features: [
+ {
+ categories: [{ color: '#ff0000', name: 'Java' }],
+ description: '10 new Java rules'
+ }
+ ]
+ };
+ return shallow(
+ <NavLatestNotification
+ lastNews={lastNews}
+ notificationsLastReadDate={parseDate('2018-01-01T12:07:19+0000')}
+ notificationsOptOut={false}
+ onClick={jest.fn()}
+ setCurrentUserSetting={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx b/server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx
new file mode 100644
index 00000000000..f9b92ec27d0
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { parseDate } from 'sonar-ui-common/helpers/dates';
+import NotificationsSidebar, {
+ Feature,
+ isUnread,
+ Notification,
+ Props
+} from '../NotificationsSidebar';
+
+const news: Props['news'] = [
+ {
+ notification: '10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration',
+ publicationDate: '2018-04-06',
+ features: [
+ {
+ categories: [
+ { color: '#ff0000', name: 'Java' },
+ { color: '#00ff00', name: 'Rules' }
+ ],
+ description: '10 new Java rules'
+ },
+ {
+ categories: [{ color: '#0000ff', name: 'BitBucket' }],
+ description: 'BitBucket branch decoration',
+ readMore: 'http://example.com'
+ }
+ ]
+ },
+ {
+ notification: 'Some other notification',
+ publicationDate: '2018-04-05',
+ features: [
+ {
+ categories: [{ color: '#0000ff', name: 'BitBucket' }],
+ description: 'BitBucket branch decoration',
+ readMore: 'http://example.com'
+ }
+ ]
+ }
+];
+
+describe('#NotificationSidebar', () => {
+ it('should render correctly if there are new features', () => {
+ const wrapper = shallowRender({ loading: true });
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setProps({ loading: false });
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('Notification')).toHaveLength(2);
+ });
+
+ it('should render correctly if there are no new unread features', () => {
+ const wrapper = shallowRender({
+ notificationsLastReadDate: parseDate('2018-12-31')
+ });
+ expect(wrapper.find('Notification')).toHaveLength(2);
+ expect(wrapper.find('Notification[unread=true]')).toHaveLength(0);
+ });
+});
+
+describe('#isUnread', () => {
+ it('should be unread', () => {
+ expect(isUnread(0, '2018-12-14', undefined)).toBe(true);
+ expect(isUnread(1, '2018-12-14', parseDate('2018-12-12'))).toBe(true);
+ });
+
+ it('should be read', () => {
+ expect(isUnread(0, '2018-12-16', parseDate('2018-12-16'))).toBe(false);
+ expect(isUnread(1, '2018-12-15', undefined)).toBe(false);
+ });
+});
+
+describe('#Notification', () => {
+ it('should render correctly', () => {
+ expect(shallow(<Notification notification={news[1]} unread={false} />)).toMatchSnapshot();
+ expect(shallow(<Notification notification={news[1]} unread={true} />)).toMatchSnapshot();
+ });
+});
+
+describe('#Feature', () => {
+ it('should render correctly', () => {
+ expect(shallow(<Feature feature={news[1].features[0]} />)).toMatchSnapshot();
+ expect(shallow(<Feature feature={news[0].features[0]} />)).toMatchSnapshot();
+ });
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
+ <NotificationsSidebar
+ fetchMoreFeatureNews={jest.fn()}
+ loading={false}
+ loadingMore={false}
+ news={news}
+ notificationsLastReadDate={parseDate('2018-01-01')}
+ onClose={jest.fn()}
+ paging={{ pageIndex: 1, pageSize: 10, total: 20 }}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
new file mode 100644
index 00000000000..c748dbfcf16
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
@@ -0,0 +1,82 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly if there are new features, and the user has not opted out 1`] = `
+<Fragment>
+ <li
+ className="navbar-latest-notification"
+ onClick={[MockFunction]}
+ >
+ <div
+ className="navbar-latest-notification-wrapper"
+ >
+ <span
+ className="badge badge-info"
+ >
+ new
+ </span>
+ <span
+ className="label"
+ >
+ 10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration
+ </span>
+ </div>
+ </li>
+ <li
+ className="navbar-latest-notification-dismiss"
+ >
+ <a
+ className="navbar-icon"
+ href="#"
+ onClick={[Function]}
+ >
+ <ClearIcon
+ size={12}
+ thin={true}
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ className="navbar-icon"
+ href="#"
+ onClick={[Function]}
+ >
+ <NotificationIcon
+ hasUnread={true}
+ />
+ </a>
+ </li>
+</Fragment>
+`;
+
+exports[`should render correctly if there are new features, but the user has opted out 1`] = `
+<Fragment>
+ <li>
+ <a
+ className="navbar-icon"
+ href="#"
+ onClick={[Function]}
+ >
+ <NotificationIcon
+ hasUnread={false}
+ />
+ </a>
+ </li>
+</Fragment>
+`;
+
+exports[`should render correctly if there are no new unread features 1`] = `
+<Fragment>
+ <li>
+ <a
+ className="navbar-icon"
+ href="#"
+ onClick={[Function]}
+ >
+ <NotificationIcon
+ hasUnread={false}
+ />
+ </a>
+ </li>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap
new file mode 100644
index 00000000000..4936ee19ea8
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap
@@ -0,0 +1,269 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`#Feature should render correctly 1`] = `
+<div
+ className="feature"
+>
+ <ul
+ className="categories spacer-bottom"
+ >
+ <li
+ key="BitBucket"
+ style={
+ Object {
+ "backgroundColor": "#0000ff",
+ }
+ }
+ >
+ BitBucket
+ </li>
+ </ul>
+ <span>
+ BitBucket branch decoration
+ </span>
+ <a
+ className="learn-more"
+ href="http://example.com"
+ rel="noopener noreferrer nofollow"
+ target="_blank"
+ >
+ learn_more
+ </a>
+</div>
+`;
+
+exports[`#Feature should render correctly 2`] = `
+<div
+ className="feature"
+>
+ <ul
+ className="categories spacer-bottom"
+ >
+ <li
+ key="Java"
+ style={
+ Object {
+ "backgroundColor": "#ff0000",
+ }
+ }
+ >
+ Java
+ </li>
+ <li
+ key="Rules"
+ style={
+ Object {
+ "backgroundColor": "#00ff00",
+ }
+ }
+ >
+ Rules
+ </li>
+ </ul>
+ <span>
+ 10 new Java rules
+ </span>
+</div>
+`;
+
+exports[`#Notification should render correctly 1`] = `
+<div
+ className="notifications-sidebar-slice"
+>
+ <h4>
+ <DateFormatter
+ date="2018-04-05"
+ long={false}
+ />
+ </h4>
+ <Feature
+ feature={
+ Object {
+ "categories": Array [
+ Object {
+ "color": "#0000ff",
+ "name": "BitBucket",
+ },
+ ],
+ "description": "BitBucket branch decoration",
+ "readMore": "http://example.com",
+ }
+ }
+ key="0"
+ />
+</div>
+`;
+
+exports[`#Notification should render correctly 2`] = `
+<div
+ className="notifications-sidebar-slice unread"
+>
+ <h4>
+ <DateFormatter
+ date="2018-04-05"
+ long={false}
+ />
+ </h4>
+ <Feature
+ feature={
+ Object {
+ "categories": Array [
+ Object {
+ "color": "#0000ff",
+ "name": "BitBucket",
+ },
+ ],
+ "description": "BitBucket branch decoration",
+ "readMore": "http://example.com",
+ }
+ }
+ key="0"
+ />
+</div>
+`;
+
+exports[`#NotificationSidebar should render correctly if there are new features 1`] = `
+<Modal
+ contentLabel="embed_docs.whats_new"
+ onRequestClose={[MockFunction]}
+>
+ <div
+ className="notifications-sidebar"
+ >
+ <div
+ className="notifications-sidebar-top"
+ >
+ <h3>
+ embed_docs.whats_new
+ </h3>
+ <ClearButton
+ className="button-tiny"
+ iconProps={
+ Object {
+ "size": 12,
+ "thin": true,
+ }
+ }
+ onClick={[MockFunction]}
+ />
+ </div>
+ <div
+ className="notifications-sidebar-content"
+ >
+ <div
+ className="text-center"
+ >
+ <DeferredSpinner
+ className="big-spacer-top"
+ timeout={200}
+ />
+ </div>
+ </div>
+ </div>
+</Modal>
+`;
+
+exports[`#NotificationSidebar should render correctly if there are new features 2`] = `
+<Modal
+ contentLabel="embed_docs.whats_new"
+ onRequestClose={[MockFunction]}
+>
+ <div
+ className="notifications-sidebar"
+ >
+ <div
+ className="notifications-sidebar-top"
+ >
+ <h3>
+ embed_docs.whats_new
+ </h3>
+ <ClearButton
+ className="button-tiny"
+ iconProps={
+ Object {
+ "size": 12,
+ "thin": true,
+ }
+ }
+ onClick={[MockFunction]}
+ />
+ </div>
+ <div
+ className="notifications-sidebar-content"
+ >
+ <Notification
+ key="2018-04-06"
+ notification={
+ Object {
+ "features": Array [
+ Object {
+ "categories": Array [
+ Object {
+ "color": "#ff0000",
+ "name": "Java",
+ },
+ Object {
+ "color": "#00ff00",
+ "name": "Rules",
+ },
+ ],
+ "description": "10 new Java rules",
+ },
+ Object {
+ "categories": Array [
+ Object {
+ "color": "#0000ff",
+ "name": "BitBucket",
+ },
+ ],
+ "description": "BitBucket branch decoration",
+ "readMore": "http://example.com",
+ },
+ ],
+ "notification": "10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration",
+ "publicationDate": "2018-04-06",
+ }
+ }
+ unread={true}
+ />
+ <Notification
+ key="2018-04-05"
+ notification={
+ Object {
+ "features": Array [
+ Object {
+ "categories": Array [
+ Object {
+ "color": "#0000ff",
+ "name": "BitBucket",
+ },
+ ],
+ "description": "BitBucket branch decoration",
+ "readMore": "http://example.com",
+ },
+ ],
+ "notification": "Some other notification",
+ "publicationDate": "2018-04-05",
+ }
+ }
+ unread={true}
+ />
+ </div>
+ <div
+ className="notifications-sidebar-footer"
+ >
+ <div
+ className="spacer-top note text-center"
+ >
+ <a
+ className="spacer-left"
+ href="#"
+ onClick={[MockFunction]}
+ >
+ show_more
+ </a>
+ </div>
+ </div>
+ </div>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/notifications/notifications.css b/server/sonar-web/src/main/js/app/components/notifications/notifications.css
new file mode 100644
index 00000000000..ef18020bf4f
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/notifications/notifications.css
@@ -0,0 +1,157 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+.navbar-latest-notification {
+ flex: 0 1 240px;
+ text-align: right;
+ overflow: hidden;
+}
+
+.navbar-latest-notification-wrapper {
+ position: relative;
+ display: inline-block;
+ padding: var(--gridSize);
+ padding-left: 50px;
+ height: 28px;
+ max-width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
+ vertical-align: middle;
+ font-size: var(--smallFontSize);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--sonarcloudBlack500);
+ background-color: #000;
+ border-radius: 3px 0 0 3px;
+ cursor: pointer;
+}
+
+.navbar-latest-notification-wrapper:hover {
+ color: var(--sonarcloudBlack300);
+}
+
+.navbar-latest-notification-wrapper .badge-info {
+ position: absolute;
+ margin-right: var(--gridSize);
+ left: 6px;
+ top: 6px;
+}
+
+.navbar-latest-notification-wrapper .label {
+ display: block;
+ max-width: 330px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.navbar-latest-notification-dismiss .navbar-icon {
+ height: 28px;
+ background-color: #000;
+ border-radius: 0 3px 3px 0;
+ padding: var(--gridSize) 7px !important;
+ margin-left: 1px;
+ margin-right: var(--gridSize);
+ color: var(--sonarcloudBlack500) !important;
+}
+
+.navbar-latest-notification-dismiss .navbar-icon:hover {
+ color: var(--sonarcloudBlack300) !important;
+}
+
+.notifications-sidebar {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 400px;
+ display: flex;
+ flex-direction: column;
+ background: var(--sonarcloudBlack200);
+}
+
+.notifications-sidebar-top {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: calc(2 * var(--gridSize));
+ border-bottom: 1px solid var(--sonarcloudBlack250);
+ background-color: var(--sonarcloudBlack100);
+}
+
+.notifications-sidebar-top h3 {
+ font-weight: normal;
+ font-size: var(--bigFontSize);
+}
+
+.notifications-sidebar-content {
+ flex: 1 1;
+ overflow-y: auto;
+}
+
+.notifications-sidebar-footer {
+ padding-top: var(--gridSize);
+ border-top: 1px solid var(--sonarcloudBlack250);
+ flex: 0 0 40px;
+}
+
+.notifications-sidebar-slice h4 {
+ padding: calc(2 * var(--gridSize));
+ padding-bottom: calc(var(--gridSize) / 2);
+ background-color: var(--sonarcloudBlack200);
+ font-weight: normal;
+ font-size: var(--smallFontSize);
+ text-align: right;
+ color: var(--sonarcloudBlack500);
+}
+
+.notifications-sidebar-slice .feature:last-of-type {
+ border-bottom: 1px solid var(--sonarcloudBlack250);
+}
+
+.notifications-sidebar-slice .feature {
+ padding: calc(2 * var(--gridSize));
+ background-color: var(--sonarcloudBlack100);
+ border-top: 1px solid var(--sonarcloudBlack250);
+ overflow: hidden;
+}
+
+.notifications-sidebar-slice.unread .feature {
+ background-color: #e6f6ff;
+ border-color: #cee4f2;
+}
+
+.notifications-sidebar-slice .learn-more {
+ clear: both;
+ float: right;
+ margin-top: var(--gridSize);
+}
+
+.notifications-sidebar-slice .categories li {
+ display: inline-block;
+ padding: 4px;
+ margin-right: var(--gridSize);
+ font-size: 9px;
+ line-height: 8px;
+ text-transform: uppercase;
+ font-weight: bold;
+ color: #fff;
+ border-radius: 3px;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
index ed9efc5b521..32a1c54100d 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
@@ -34,7 +34,6 @@ import BranchList from './BranchList';
import ProjectBaselineSelector from './ProjectBaselineSelector';
interface Props {
- branchLike: Branch;
branchLikes: BranchLike[];
branchesEnabled?: boolean;
canAdmin?: boolean;
@@ -121,15 +120,13 @@ export default class App extends React.PureComponent<Props, State> {
}
fetchLeakPeriodSetting() {
- const { branchLike, branchesEnabled, component } = this.props;
-
this.setState({ loading: true });
Promise.all([
getNewCodePeriod(),
getNewCodePeriod({
- branch: branchesEnabled ? undefined : branchLike.name,
- project: component.key
+ branch: !this.props.branchesEnabled ? 'master' : undefined,
+ project: this.props.component.key
})
]).then(
([generalSetting, setting]) => {
@@ -229,7 +226,7 @@ export default class App extends React.PureComponent<Props, State> {
};
render() {
- const { branchesEnabled, canAdmin, component, branchLike } = this.props;
+ const { branchesEnabled, canAdmin, component } = this.props;
const {
analysis,
branchList,
@@ -259,7 +256,6 @@ export default class App extends React.PureComponent<Props, State> {
{generalSetting && overrideGeneralSetting !== undefined && (
<ProjectBaselineSelector
analysis={analysis}
- branch={branchLike}
branchList={branchList}
branchesEnabled={branchesEnabled}
component={component.key}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
index 0083374fb70..2b63146fa7f 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
@@ -27,7 +27,7 @@ import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';
interface Props {
analysis: string;
- branch?: string;
+ branch: string;
component: string;
onSelectAnalysis: (analysis: T.ParsedAnalysis) => void;
}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
index 4d133b25ee1..574ce1a048d 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
@@ -34,7 +34,6 @@ import BranchAnalysisList from './BranchAnalysisList';
export interface ProjectBaselineSelectorProps {
analysis?: string;
- branch: Branch;
branchList: Branch[];
branchesEnabled?: boolean;
component: string;
@@ -83,7 +82,6 @@ function branchToOption(b: Branch) {
export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) {
const {
analysis,
- branch,
branchList,
branchesEnabled,
component,
@@ -165,7 +163,7 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
{selected === 'SPECIFIC_ANALYSIS' && (
<BranchAnalysisList
analysis={analysis || ''}
- branch={branch.name}
+ branch="master"
component={component}
onSelectAnalysis={props.onSelectAnalysis}
/>
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
index 65cd8e22f08..f4c3fd6bc94 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
@@ -35,14 +35,8 @@ jest.mock('../../../../api/newCodePeriod', () => ({
setNewCodePeriod: jest.fn().mockResolvedValue({})
}));
-it('should render correctly', async () => {
- let wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-
- wrapper = shallowRender({ branchesEnabled: false });
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot('without branch support');
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
});
it('should initialize correctly', async () => {
@@ -106,7 +100,6 @@ it('should handle errors gracefully', async () => {
function shallowRender(props: Partial<App['props']> = {}) {
return shallow<App>(
<App
- branchLike={mockBranch()}
branchLikes={[mockMainBranch()]}
branchesEnabled={true}
canAdmin={true}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
index 294b63939ee..0e1cf612c5a 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
@@ -19,7 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
+import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import ProjectBaselineSelector, { ProjectBaselineSelectorProps } from '../ProjectBaselineSelector';
it('should render correctly', () => {
@@ -105,7 +105,6 @@ it('should disable the save button when date is invalid', () => {
function shallowRender(props: Partial<ProjectBaselineSelectorProps> = {}) {
return shallow(
<ProjectBaselineSelector
- branch={mockBranch()}
branchList={[mockMainBranch()]}
branchesEnabled={true}
component=""
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
index e642a0c6573..7b76b1ebb90 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -11,186 +11,7 @@ exports[`should render correctly 1`] = `
<AppHeader
canAdmin={true}
/>
- <div
- className="panel-white project-baseline"
- >
- <h2>
- project_baseline.default_setting
- </h2>
- <ProjectBaselineSelector
- analysis=""
- branch={
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": false,
- "name": "branch-6.7",
- }
- }
- branchList={
- Array [
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": true,
- "name": "master",
- },
- ]
- }
- branchesEnabled={true}
- component="my-project"
- currentSetting="PREVIOUS_VERSION"
- days="30"
- generalSetting={
- Object {
- "type": "PREVIOUS_VERSION",
- }
- }
- onCancel={[Function]}
- onSelectAnalysis={[Function]}
- onSelectDays={[Function]}
- onSelectReferenceBranch={[Function]}
- onSelectSetting={[Function]}
- onSubmit={[Function]}
- onToggleSpecificSetting={[Function]}
- overrideGeneralSetting={true}
- referenceBranch="master"
- saving={false}
- selected="PREVIOUS_VERSION"
- />
- <div
- className="spacer-top invisible"
- >
- <span
- className="text-success"
- >
- <AlertSuccessIcon
- className="spacer-right"
- />
- settings.state.saved
- </span>
- </div>
- <div
- className="huge-spacer-top branch-baseline-selector"
- >
- <hr />
- <h2>
- project_baseline.configure_branches
- </h2>
- <BranchList
- branchList={
- Array [
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": true,
- "name": "master",
- },
- ]
- }
- component={
- Object {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "qualifier": "TRK",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- }
- }
- inheritedSetting={
- Object {
- "type": "PREVIOUS_VERSION",
- "value": undefined,
- }
- }
- />
- </div>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`should render correctly: without branch support 1`] = `
-<Fragment>
- <Suggestions
- suggestions="project_baseline"
- />
- <div
- className="page page-limited"
- >
- <AppHeader
- canAdmin={true}
- />
- <div
- className="panel-white project-baseline"
- >
- <ProjectBaselineSelector
- analysis=""
- branch={
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": false,
- "name": "branch-6.7",
- }
- }
- branchList={
- Array [
- Object {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": true,
- "name": "master",
- },
- ]
- }
- branchesEnabled={false}
- component="my-project"
- currentSetting="PREVIOUS_VERSION"
- days="30"
- generalSetting={
- Object {
- "type": "PREVIOUS_VERSION",
- }
- }
- onCancel={[Function]}
- onSelectAnalysis={[Function]}
- onSelectDays={[Function]}
- onSelectReferenceBranch={[Function]}
- onSelectSetting={[Function]}
- onSubmit={[Function]}
- onToggleSpecificSetting={[Function]}
- overrideGeneralSetting={true}
- referenceBranch="master"
- saving={false}
- selected="PREVIOUS_VERSION"
- />
- <div
- className="spacer-top invisible"
- >
- <span
- className="text-success"
- >
- <AlertSuccessIcon
- className="spacer-right"
- />
- settings.state.saved
- </span>
- </div>
- </div>
+ <DeferredSpinner />
</div>
</Fragment>
`;