aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components
diff options
context:
space:
mode:
authorSiegfried Ehret <49895321+siegfried-ehret-sonarsource@users.noreply.github.com>2019-10-17 11:32:10 +0200
committerSonarTech <sonartech@sonarsource.com>2019-10-28 20:21:09 +0100
commitcd2296809f5308f0cbf14100fcbff2846c3bf3da (patch)
tree9be140071398becdef60caf20b6555bba31e1ebd /server/sonar-web/src/main/js/components
parent3c93ddc45bbd9210d55a9d6b7a8802e330247640 (diff)
downloadsonarqube-cd2296809f5308f0cbf14100fcbff2846c3bf3da.tar.gz
sonarqube-cd2296809f5308f0cbf14100fcbff2846c3bf3da.zip
SONAR-10037 Manage project notifications from project dashboard
Co-authored-by: Wouter Admiraal <wouter.admiraal@sonarsource.com>
Diffstat (limited to 'server/sonar-web/src/main/js/components')
-rw-r--r--server/sonar-web/src/main/js/components/hoc/__tests__/__snapshots__/withNotifications-test.tsx.snap51
-rw-r--r--server/sonar-web/src/main/js/components/hoc/__tests__/withNotifications-test.tsx96
-rw-r--r--server/sonar-web/src/main/js/components/hoc/withNotifications.tsx148
3 files changed, 295 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/__snapshots__/withNotifications-test.tsx.snap b/server/sonar-web/src/main/js/components/hoc/__tests__/__snapshots__/withNotifications-test.tsx.snap
new file mode 100644
index 00000000000..4af5332cd6c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/hoc/__tests__/__snapshots__/withNotifications-test.tsx.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should fetch notifications and render 1`] = `
+<X
+ addNotification={[Function]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ globalTypes={
+ Array [
+ "type-global",
+ "type-common",
+ ]
+ }
+ loading={false}
+ notifications={
+ Array [
+ Object {
+ "channel": "channel1",
+ "organization": "org",
+ "project": "foo",
+ "projectName": "Foo",
+ "type": "type-global",
+ },
+ Object {
+ "channel": "channel1",
+ "organization": "org",
+ "project": "bar",
+ "projectName": "Bar",
+ "type": "type-common",
+ },
+ Object {
+ "channel": "channel2",
+ "organization": "org",
+ "project": "qux",
+ "projectName": "Qux",
+ "type": "type-common",
+ },
+ ]
+ }
+ perProjectTypes={
+ Array [
+ "type-common",
+ ]
+ }
+ removeNotification={[Function]}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/withNotifications-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/withNotifications-test.tsx
new file mode 100644
index 00000000000..7d192fe72ef
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/hoc/__tests__/withNotifications-test.tsx
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { addNotification, getNotifications, removeNotification } from '../../../api/notifications';
+import { withNotifications, WithNotificationsProps } from '../withNotifications';
+
+jest.mock('../../../api/notifications', () => ({
+ addNotification: jest.fn().mockResolvedValue({}),
+ getNotifications: jest.fn(() =>
+ Promise.resolve({
+ channels: ['channel1', 'channel2'],
+ globalTypes: ['type-global', 'type-common'],
+ notifications: [
+ {
+ channel: 'channel1',
+ type: 'type-global',
+ project: 'foo',
+ projectName: 'Foo',
+ organization: 'org'
+ },
+ {
+ channel: 'channel1',
+ type: 'type-common',
+ project: 'bar',
+ projectName: 'Bar',
+ organization: 'org'
+ },
+ {
+ channel: 'channel2',
+ type: 'type-common',
+ project: 'qux',
+ projectName: 'Qux',
+ organization: 'org'
+ }
+ ],
+ perProjectTypes: ['type-common']
+ })
+ ),
+ removeNotification: jest.fn().mockResolvedValue({})
+}));
+
+class X extends React.Component<WithNotificationsProps> {
+ render() {
+ return <div />;
+ }
+}
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('should fetch notifications and render', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+ expect(getNotifications).toBeCalled();
+});
+
+it('should add and remove a notification', () => {
+ const wrapper = shallowRender();
+ const notification = {
+ channel: 'EmailNotificationChannel',
+ project: 'foo',
+ type: 'SQ-MyNewIssues'
+ };
+
+ wrapper.prop('addNotification')(notification);
+ expect(addNotification).toHaveBeenCalledWith(notification);
+
+ wrapper.prop('removeNotification')(notification);
+ expect(removeNotification).toHaveBeenCalledWith(notification);
+});
+
+function shallowRender() {
+ const UnderTest = withNotifications<{}>(X);
+ return shallow(<UnderTest />);
+}
diff --git a/server/sonar-web/src/main/js/components/hoc/withNotifications.tsx b/server/sonar-web/src/main/js/components/hoc/withNotifications.tsx
new file mode 100644
index 00000000000..f327c46f38e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/hoc/withNotifications.tsx
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { uniqWith } from 'lodash';
+import * as React from 'react';
+import { addNotification, getNotifications, removeNotification } from '../../api/notifications';
+import { getWrappedDisplayName } from './utils';
+
+interface State {
+ channels: string[];
+ globalTypes: string[];
+ loading: boolean;
+ notifications: T.Notification[];
+ perProjectTypes: string[];
+}
+
+export interface WithNotificationsProps {
+ addNotification: (added: T.Notification) => void;
+ channels: string[];
+ globalTypes: string[];
+ loading: boolean;
+ notifications: T.Notification[];
+ perProjectTypes: string[];
+ removeNotification: (removed: T.Notification) => void;
+}
+
+export function withNotifications<P>(
+ WrappedComponent: React.ComponentType<P & WithNotificationsProps>
+) {
+ class Wrapper extends React.Component<P, State> {
+ mounted = false;
+ static displayName = getWrappedDisplayName(WrappedComponent, 'withNotifications');
+
+ state: State = {
+ channels: [],
+ globalTypes: [],
+ loading: true,
+ notifications: [],
+ perProjectTypes: []
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchNotifications();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchNotifications = () => {
+ getNotifications().then(
+ response => {
+ if (this.mounted) {
+ this.setState({
+ channels: response.channels,
+ globalTypes: response.globalTypes,
+ loading: false,
+ notifications: response.notifications,
+ perProjectTypes: response.perProjectTypes
+ });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ addNotificationToState = (added: T.Notification) => {
+ this.setState(state => {
+ const notifications = uniqWith([...state.notifications, added], this.areNotificationsEqual);
+ return { notifications };
+ });
+ };
+
+ removeNotificationFromState = (removed: T.Notification) => {
+ this.setState(state => {
+ const notifications = state.notifications.filter(
+ notification => !this.areNotificationsEqual(notification, removed)
+ );
+ return { notifications };
+ });
+ };
+
+ addNotification = (added: T.Notification) => {
+ // optimistic update
+ this.addNotificationToState(added);
+
+ // recreate `data` to omit `projectName` and `organization` from `Notification`
+ const data = { channel: added.channel, project: added.project, type: added.type };
+ addNotification(data).catch(() => {
+ this.removeNotificationFromState(added);
+ });
+ };
+
+ removeNotification = (removed: T.Notification) => {
+ // optimistic update
+ this.removeNotificationFromState(removed);
+
+ // recreate `data` to omit `projectName` and `organization` from `Notification`
+ const data = { channel: removed.channel, project: removed.project, type: removed.type };
+ removeNotification(data).catch(() => {
+ this.addNotificationToState(removed);
+ });
+ };
+
+ areNotificationsEqual = (a: T.Notification, b: T.Notification) => {
+ return a.channel === b.channel && a.type === b.type && a.project === b.project;
+ };
+
+ render() {
+ const { channels, globalTypes, loading, notifications, perProjectTypes } = this.state;
+ return (
+ <WrappedComponent
+ {...this.props}
+ addNotification={this.addNotification}
+ channels={channels}
+ globalTypes={globalTypes}
+ loading={loading}
+ notifications={notifications}
+ perProjectTypes={perProjectTypes}
+ removeNotification={this.removeNotification}
+ />
+ );
+ }
+ }
+
+ return Wrapper;
+}