]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13862 Allow analysis messages to be permanently dismissed
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Mon, 14 Sep 2020 15:00:12 +0000 (17:00 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 29 Sep 2020 20:07:42 +0000 (20:07 +0000)
21 files changed:
server/sonar-web/src/main/js/api/ce.ts
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx
server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap
server/sonar-web/src/main/js/app/styles/init/links.css
server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap
server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx
server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap
server/sonar-web/src/main/js/helpers/mocks/tasks.ts
server/sonar-web/src/main/js/types/tasks.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index e2b9a1f846cdf44796f5810d5b4bd3dd7fa99c75..35f43ef9e1de1b9fe1d6216fbc185556ca152993 100644 (file)
@@ -20,7 +20,7 @@
 import { getJSON, post, RequestData } from 'sonar-ui-common/helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
 import { IndexationStatus } from '../types/indexation';
-import { Task } from '../types/tasks';
+import { Task, TaskWarning } from '../types/tasks';
 
 export function getAnalysisStatus(data: {
   component: string;
@@ -33,7 +33,7 @@ export function getAnalysisStatus(data: {
     name: string;
     organization?: string;
     pullRequest?: string;
-    warnings: string[];
+    warnings: TaskWarning[];
   };
 }> {
   return getJSON('/api/ce/analysis_status', data).catch(throwGlobalError);
@@ -87,3 +87,7 @@ export function setWorkerCount(count: number): Promise<void | Response> {
 export function getIndexationStatus(): Promise<IndexationStatus> {
   return getJSON('/api/ce/indexation_status').catch(throwGlobalError);
 }
+
+export function dismissAnalysisWarning(component: string, warning: string) {
+  return post('/api/ce/dismiss_analysis_warning', { component, warning }).catch(throwGlobalError);
+}
index dc540a9b5b412da26859170a6a6a06c410ff5f98..02ac49bfea04bf0c89d96cb5def00d1d89e506ed 100644 (file)
@@ -39,7 +39,7 @@ import {
 } from '../../store/rootActions';
 import { BranchLike } from '../../types/branch-like';
 import { isPortfolioLike } from '../../types/component';
-import { Task, TaskStatuses } from '../../types/tasks';
+import { Task, TaskStatuses, TaskWarning } from '../../types/tasks';
 import ComponentContainerNotFound from './ComponentContainerNotFound';
 import { ComponentContext } from './ComponentContext';
 import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation';
@@ -62,7 +62,7 @@ interface State {
   isPending: boolean;
   loading: boolean;
   tasksInProgress?: Task[];
-  warnings: string[];
+  warnings: TaskWarning[];
 }
 
 const FETCH_STATUS_WAIT_TIME = 3000;
@@ -320,6 +320,13 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
     }
   };
 
+  handleWarningDismiss = () => {
+    const { component } = this.state;
+    if (component !== undefined) {
+      this.fetchWarnings(component);
+    }
+  };
+
   render() {
     const { component, loading } = this.state;
 
@@ -346,6 +353,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
             isInProgress={isInProgress}
             isPending={isPending}
             onComponentChange={this.handleComponentChange}
+            onWarningDismiss={this.handleWarningDismiss}
             warnings={this.state.warnings}
           />
         )}
index 0a23c0441385ff5adcc11997647a36da792809a9..36bebb2c835109b5cc1ff6dbd3d8ad21fc296fac 100644 (file)
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
 import { getBranches, getPullRequests } from '../../../api/branches';
-import { getTasksForComponent } from '../../../api/ce';
+import { getAnalysisStatus, getTasksForComponent } from '../../../api/ce';
 import { getComponentData } from '../../../api/components';
 import { getComponentNavigation } from '../../../api/nav';
 import { mockBranch, mockMainBranch, mockPullRequest } from '../../../helpers/mocks/branch-like';
@@ -274,6 +274,22 @@ it('should display display the unavailable page if the component needs issue syn
   expect(wrapper.find(PageUnavailableDueToIndexation).exists()).toBe(true);
 });
 
+it('should correctly reload last task warnings if anything got dismissed', async () => {
+  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
+    component: mockComponent({
+      breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }]
+    })
+  });
+  (getComponentNavigation as jest.Mock).mockResolvedValueOnce({});
+
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  (getAnalysisStatus as jest.Mock).mockClear();
+
+  wrapper.instance().handleWarningDismiss();
+  expect(getAnalysisStatus).toBeCalledTimes(1);
+});
+
 function shallowRender(props: Partial<ComponentContainer['props']> = {}) {
   return shallow<ComponentContainer>(
     <ComponentContainer
index 1e0f7fb0455cef70a9e19dd9290f478307301074..e1a30fe8c3a85d2f6e0a51c52a0de3e86e27912a 100644 (file)
@@ -22,7 +22,7 @@ import * as React from 'react';
 import ContextNavBar from 'sonar-ui-common/components/ui/ContextNavBar';
 import { BranchLike } from '../../../../types/branch-like';
 import { ComponentQualifier } from '../../../../types/component';
-import { Task, TaskStatuses } from '../../../../types/tasks';
+import { Task, TaskStatuses, TaskWarning } from '../../../../types/tasks';
 import { rawSizes } from '../../../theme';
 import RecentHistory from '../../RecentHistory';
 import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif';
@@ -32,7 +32,7 @@ import Menu from './Menu';
 import InfoDrawer from './projectInformation/InfoDrawer';
 import ProjectInformation from './projectInformation/ProjectInformation';
 
-interface Props {
+export interface ComponentNavProps {
   branchLikes: BranchLike[];
   currentBranchLike: BranchLike | undefined;
   component: T.Component;
@@ -41,10 +41,11 @@ interface Props {
   isInProgress?: boolean;
   isPending?: boolean;
   onComponentChange: (changes: Partial<T.Component>) => void;
-  warnings: string[];
+  onWarningDismiss: () => void;
+  warnings: TaskWarning[];
 }
 
-export default function ComponentNav(props: Props) {
+export default function ComponentNav(props: ComponentNavProps) {
   const {
     branchLikes,
     component,
@@ -100,7 +101,12 @@ export default function ComponentNav(props: Props) {
           component={component}
           currentBranchLike={currentBranchLike}
         />
-        <HeaderMeta branchLike={currentBranchLike} component={component} warnings={warnings} />
+        <HeaderMeta
+          branchLike={currentBranchLike}
+          component={component}
+          onWarningDismiss={props.onWarningDismiss}
+          warnings={warnings}
+        />
       </div>
       <Menu
         branchLike={currentBranchLike}
index f377731fb6fcd13e6fdb1e48c8afa4560eba6e75..dea333111b11588503e3ede3f9977eee77d035cf 100644 (file)
@@ -22,6 +22,7 @@ import { FormattedMessage } from 'react-intl';
 import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent';
 import { Alert } from 'sonar-ui-common/components/ui/Alert';
 import { translate } from 'sonar-ui-common/helpers/l10n';
+import { TaskWarning } from '../../../../types/tasks';
 
 const AnalysisWarningsModal = lazyLoadComponent(
   () => import('../../../../components/common/AnalysisWarningsModal'),
@@ -29,7 +30,9 @@ const AnalysisWarningsModal = lazyLoadComponent(
 );
 
 interface Props {
-  warnings: string[];
+  componentKey: string;
+  onWarningDismiss: () => void;
+  warnings: TaskWarning[];
 }
 
 interface State {
@@ -72,7 +75,12 @@ export default class ComponentNavWarnings extends React.PureComponent<Props, Sta
           />
         </Alert>
         {this.state.modal && (
-          <AnalysisWarningsModal onClose={this.handleCloseModal} warnings={this.props.warnings} />
+          <AnalysisWarningsModal
+            componentKey={this.props.componentKey}
+            onClose={this.handleCloseModal}
+            onWarningDismiss={this.props.onWarningDismiss}
+            warnings={this.props.warnings}
+          />
         )}
       </>
     );
index 5706d62927c8a3bf79a2cde587b12ba0425c862a..4d8d4763a05ff6a01f96439833a7b3f6022a9754 100644 (file)
@@ -29,6 +29,7 @@ import { isLoggedIn } from '../../../../helpers/users';
 import { getCurrentUser, Store } from '../../../../store/rootReducer';
 import { BranchLike } from '../../../../types/branch-like';
 import { ComponentQualifier } from '../../../../types/component';
+import { TaskWarning } from '../../../../types/tasks';
 import ComponentNavWarnings from './ComponentNavWarnings';
 import './HeaderMeta.css';
 
@@ -36,7 +37,8 @@ export interface HeaderMetaProps {
   branchLike?: BranchLike;
   currentUser: T.CurrentUser;
   component: T.Component;
-  warnings: string[];
+  onWarningDismiss: () => void;
+  warnings: TaskWarning[];
 }
 
 export function HeaderMeta(props: HeaderMetaProps) {
@@ -51,7 +53,11 @@ export function HeaderMeta(props: HeaderMetaProps) {
       <div className="display-flex-center flex-0 small">
         {warnings.length > 0 && (
           <span className="header-meta-warnings">
-            <ComponentNavWarnings warnings={warnings} />
+            <ComponentNavWarnings
+              componentKey={component.key}
+              onWarningDismiss={props.onWarningDismiss}
+              warnings={warnings}
+            />
           </span>
         )}
         {component.analysisDate && (
index 205f7bf64ad3ffbe8f81d7cb94bfd23372ce4f81..c14e8a43ebb040f16cba598188848f083a8a4dd0 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import ComponentNav from '../ComponentNav';
-
-const component = {
-  breadcrumbs: [{ key: 'component', name: 'component', qualifier: 'TRK' }],
-  key: 'component',
-  name: 'component',
-  organization: 'org',
-  qualifier: 'TRK'
-};
-
-it('renders', () => {
-  const wrapper = shallow(
+import { mockTask, mockTaskWarning } from '../../../../../helpers/mocks/tasks';
+import { mockComponent } from '../../../../../helpers/testMocks';
+import { ComponentQualifier } from '../../../../../types/component';
+import { TaskStatuses } from '../../../../../types/tasks';
+import RecentHistory from '../../../RecentHistory';
+import ComponentNav, { ComponentNavProps } from '../ComponentNav';
+import Menu from '../Menu';
+import InfoDrawer from '../projectInformation/InfoDrawer';
+
+beforeEach(() => {
+  jest.clearAllMocks();
+  jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f());
+});
+
+it('renders correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(shallowRender({ warnings: [mockTaskWarning()] })).toMatchSnapshot('has warnings');
+  expect(shallowRender({ isInProgress: true })).toMatchSnapshot('has in progress notification');
+  expect(shallowRender({ isPending: true })).toMatchSnapshot('has pending notification');
+  expect(shallowRender({ currentTask: mockTask({ status: TaskStatuses.Failed }) })).toMatchSnapshot(
+    'has failed notification'
+  );
+});
+
+it('correctly adds data to the history if there are breadcrumbs', () => {
+  const key = 'foo';
+  const name = 'Foo';
+  const organization = 'baz';
+  const qualifier = ComponentQualifier.Portfolio;
+  const spy = jest.spyOn(RecentHistory, 'add');
+
+  shallowRender({
+    component: mockComponent({
+      key,
+      name,
+      organization,
+      breadcrumbs: [
+        {
+          key: 'bar',
+          name: 'Bar',
+          qualifier
+        }
+      ]
+    })
+  });
+
+  expect(spy).toBeCalledWith(key, name, qualifier.toLowerCase(), organization);
+});
+
+it('correctly toggles the project info display', () => {
+  const wrapper = shallowRender();
+  expect(wrapper.find(InfoDrawer).props().displayed).toBe(false);
+
+  wrapper
+    .find(Menu)
+    .props()
+    .onToggleProjectInfo();
+  expect(wrapper.find(InfoDrawer).props().displayed).toBe(true);
+
+  wrapper
+    .find(Menu)
+    .props()
+    .onToggleProjectInfo();
+  expect(wrapper.find(InfoDrawer).props().displayed).toBe(false);
+
+  wrapper
+    .find(Menu)
+    .props()
+    .onToggleProjectInfo();
+  wrapper
+    .find(InfoDrawer)
+    .props()
+    .onClose();
+  expect(wrapper.find(InfoDrawer).props().displayed).toBe(false);
+});
+
+function shallowRender(props: Partial<ComponentNavProps> = {}) {
+  return shallow<ComponentNavProps>(
     <ComponentNav
       branchLikes={[]}
-      component={component}
+      component={mockComponent({
+        breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }]
+      })}
       currentBranchLike={undefined}
-      isInProgress={true}
-      isPending={true}
+      isInProgress={false}
+      isPending={false}
       onComponentChange={jest.fn()}
+      onWarningDismiss={jest.fn()}
       warnings={[]}
+      {...props}
     />
   );
-  expect(wrapper).toMatchSnapshot();
-});
+}
index 7bedb07be3ca2a444cfe7fd88793f138a7becde1..431bdf14d6cad5b08cd31796003db8c0fe606b4b 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { mockTaskWarning } from '../../../../../helpers/mocks/tasks';
 import ComponentNavWarnings from '../ComponentNavWarnings';
 
 it('should render', () => {
-  const wrapper = shallow(<ComponentNavWarnings warnings={['warning 1']} />);
+  const wrapper = shallow(
+    <ComponentNavWarnings
+      componentKey="foo"
+      onWarningDismiss={jest.fn()}
+      warnings={[mockTaskWarning({ message: 'warning 1' })]}
+    />
+  );
   wrapper.setState({ modal: true });
   expect(wrapper).toMatchSnapshot();
 });
index b918bdacb38ea404f3d9f53f40765600e3056a1a..a76e8cd6f884316ba1e8707a3554a948a1d96983 100644 (file)
@@ -21,6 +21,7 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import HomePageSelect from '../../../../../components/controls/HomePageSelect';
 import { mockBranch, mockPullRequest } from '../../../../../helpers/mocks/branch-like';
+import { mockTaskWarning } from '../../../../../helpers/mocks/tasks';
 import { mockComponent, mockCurrentUser } from '../../../../../helpers/testMocks';
 import { ComponentQualifier } from '../../../../../types/component';
 import { getCurrentPage, HeaderMeta, HeaderMetaProps } from '../HeaderMeta';
@@ -95,7 +96,8 @@ function shallowRender(props: Partial<HeaderMetaProps> = {}) {
       branchLike={mockBranch()}
       component={mockComponent({ analysisDate: '2017-01-02T00:00:00.000Z', version: '0.0.1' })}
       currentUser={mockCurrentUser({ isLoggedIn: true })}
-      warnings={['ERROR_1', 'ERROR_2']}
+      onWarningDismiss={jest.fn()}
+      warnings={[mockTaskWarning({ message: 'ERROR_1' }), mockTaskWarning({ message: 'ERROR_2' })]}
       {...props}
     />
   );
index e311796c90bd33a651d5dbe8d1f427bafd239614..bf8b1225b71d02c6f4c0efc6a9a2c245a8ef80c8 100644 (file)
@@ -1,6 +1,354 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`renders 1`] = `
+exports[`renders correctly: default 1`] = `
+<ContextNavBar
+  height={72}
+  id="context-navigation"
+>
+  <div
+    className="display-flex-center display-flex-space-between little-padded-top padded-bottom"
+  >
+    <Connect(Component)
+      branchLikes={Array []}
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+    />
+    <Connect(HeaderMeta)
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+      onWarningDismiss={[MockFunction]}
+      warnings={Array []}
+    />
+  </div>
+  <Connect(withAppState(Menu))
+    branchLikes={Array []}
+    component={
+      Object {
+        "breadcrumbs": Array [
+          Object {
+            "key": "foo",
+            "name": "Foo",
+            "qualifier": "TRK",
+          },
+        ],
+        "key": "my-project",
+        "name": "MyProject",
+        "organization": "foo",
+        "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 [],
+      }
+    }
+    isInProgress={false}
+    isPending={false}
+    onToggleProjectInfo={[Function]}
+  />
+  <InfoDrawer
+    displayed={false}
+    onClose={[Function]}
+    top={120}
+  >
+    <Connect(ProjectInformation)
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+      onComponentChange={[MockFunction]}
+    />
+  </InfoDrawer>
+</ContextNavBar>
+`;
+
+exports[`renders correctly: has failed notification 1`] = `
+<ContextNavBar
+  height={102}
+  id="context-navigation"
+  notif={
+    <ComponentNavBgTaskNotif
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+      currentTask={
+        Object {
+          "analysisId": "x123",
+          "componentKey": "foo",
+          "componentName": "Foo",
+          "componentQualifier": "TRK",
+          "id": "AXR8jg_0mF2ZsYr8Wzs2",
+          "organization": "bar",
+          "status": "FAILED",
+          "submittedAt": "2020-09-11T11:45:35+0200",
+          "type": "REPORT",
+        }
+      }
+      isInProgress={false}
+      isPending={false}
+    />
+  }
+>
+  <div
+    className="display-flex-center display-flex-space-between little-padded-top padded-bottom"
+  >
+    <Connect(Component)
+      branchLikes={Array []}
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+    />
+    <Connect(HeaderMeta)
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+      onWarningDismiss={[MockFunction]}
+      warnings={Array []}
+    />
+  </div>
+  <Connect(withAppState(Menu))
+    branchLikes={Array []}
+    component={
+      Object {
+        "breadcrumbs": Array [
+          Object {
+            "key": "foo",
+            "name": "Foo",
+            "qualifier": "TRK",
+          },
+        ],
+        "key": "my-project",
+        "name": "MyProject",
+        "organization": "foo",
+        "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 [],
+      }
+    }
+    isInProgress={false}
+    isPending={false}
+    onToggleProjectInfo={[Function]}
+  />
+  <InfoDrawer
+    displayed={false}
+    onClose={[Function]}
+    top={120}
+  >
+    <Connect(ProjectInformation)
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+      onComponentChange={[MockFunction]}
+    />
+  </InfoDrawer>
+</ContextNavBar>
+`;
+
+exports[`renders correctly: has in progress notification 1`] = `
 <ContextNavBar
   height={102}
   id="context-navigation"
@@ -10,19 +358,33 @@ exports[`renders 1`] = `
         Object {
           "breadcrumbs": Array [
             Object {
-              "key": "component",
-              "name": "component",
+              "key": "foo",
+              "name": "Foo",
               "qualifier": "TRK",
             },
           ],
-          "key": "component",
-          "name": "component",
-          "organization": "org",
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
           "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 [],
         }
       }
       isInProgress={true}
-      isPending={true}
+      isPending={false}
     />
   }
 >
@@ -35,15 +397,29 @@ exports[`renders 1`] = `
         Object {
           "breadcrumbs": Array [
             Object {
-              "key": "component",
-              "name": "component",
+              "key": "foo",
+              "name": "Foo",
               "qualifier": "TRK",
             },
           ],
-          "key": "component",
-          "name": "component",
-          "organization": "org",
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
           "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 [],
         }
       }
     />
@@ -52,17 +428,32 @@ exports[`renders 1`] = `
         Object {
           "breadcrumbs": Array [
             Object {
-              "key": "component",
-              "name": "component",
+              "key": "foo",
+              "name": "Foo",
               "qualifier": "TRK",
             },
           ],
-          "key": "component",
-          "name": "component",
-          "organization": "org",
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
           "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 [],
         }
       }
+      onWarningDismiss={[MockFunction]}
       warnings={Array []}
     />
   </div>
@@ -72,18 +463,217 @@ exports[`renders 1`] = `
       Object {
         "breadcrumbs": Array [
           Object {
-            "key": "component",
-            "name": "component",
+            "key": "foo",
+            "name": "Foo",
             "qualifier": "TRK",
           },
         ],
-        "key": "component",
-        "name": "component",
-        "organization": "org",
+        "key": "my-project",
+        "name": "MyProject",
+        "organization": "foo",
         "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 [],
       }
     }
     isInProgress={true}
+    isPending={false}
+    onToggleProjectInfo={[Function]}
+  />
+  <InfoDrawer
+    displayed={false}
+    onClose={[Function]}
+    top={120}
+  >
+    <Connect(ProjectInformation)
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+      onComponentChange={[MockFunction]}
+    />
+  </InfoDrawer>
+</ContextNavBar>
+`;
+
+exports[`renders correctly: has pending notification 1`] = `
+<ContextNavBar
+  height={102}
+  id="context-navigation"
+  notif={
+    <ComponentNavBgTaskNotif
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+      isInProgress={false}
+      isPending={true}
+    />
+  }
+>
+  <div
+    className="display-flex-center display-flex-space-between little-padded-top padded-bottom"
+  >
+    <Connect(Component)
+      branchLikes={Array []}
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+    />
+    <Connect(HeaderMeta)
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+      onWarningDismiss={[MockFunction]}
+      warnings={Array []}
+    />
+  </div>
+  <Connect(withAppState(Menu))
+    branchLikes={Array []}
+    component={
+      Object {
+        "breadcrumbs": Array [
+          Object {
+            "key": "foo",
+            "name": "Foo",
+            "qualifier": "TRK",
+          },
+        ],
+        "key": "my-project",
+        "name": "MyProject",
+        "organization": "foo",
+        "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 [],
+      }
+    }
+    isInProgress={false}
     isPending={true}
     onToggleProjectInfo={[Function]}
   />
@@ -97,15 +687,187 @@ exports[`renders 1`] = `
         Object {
           "breadcrumbs": Array [
             Object {
-              "key": "component",
-              "name": "component",
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+      onComponentChange={[MockFunction]}
+    />
+  </InfoDrawer>
+</ContextNavBar>
+`;
+
+exports[`renders correctly: has warnings 1`] = `
+<ContextNavBar
+  height={72}
+  id="context-navigation"
+>
+  <div
+    className="display-flex-center display-flex-space-between little-padded-top"
+  >
+    <Connect(Component)
+      branchLikes={Array []}
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
               "qualifier": "TRK",
             },
           ],
-          "key": "component",
-          "name": "component",
-          "organization": "org",
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
           "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 [],
+        }
+      }
+    />
+    <Connect(HeaderMeta)
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
+        }
+      }
+      onWarningDismiss={[MockFunction]}
+      warnings={
+        Array [
+          Object {
+            "dismissable": false,
+            "key": "foo",
+            "message": "Lorem ipsum",
+          },
+        ]
+      }
+    />
+  </div>
+  <Connect(withAppState(Menu))
+    branchLikes={Array []}
+    component={
+      Object {
+        "breadcrumbs": Array [
+          Object {
+            "key": "foo",
+            "name": "Foo",
+            "qualifier": "TRK",
+          },
+        ],
+        "key": "my-project",
+        "name": "MyProject",
+        "organization": "foo",
+        "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 [],
+      }
+    }
+    isInProgress={false}
+    isPending={false}
+    onToggleProjectInfo={[Function]}
+  />
+  <InfoDrawer
+    displayed={false}
+    onClose={[Function]}
+    top={120}
+  >
+    <Connect(ProjectInformation)
+      component={
+        Object {
+          "breadcrumbs": Array [
+            Object {
+              "key": "foo",
+              "name": "Foo",
+              "qualifier": "TRK",
+            },
+          ],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "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 [],
         }
       }
       onComponentChange={[MockFunction]}
index 03033721f88ca08b634629a6eeafef5c5e22dc27..f7893380d5b9d23792d2eb31c76f0cc49617fb33 100644 (file)
@@ -31,10 +31,16 @@ exports[`should render 1`] = `
     />
   </Alert>
   <AnalysisWarningsModal
+    componentKey="foo"
     onClose={[Function]}
+    onWarningDismiss={[MockFunction]}
     warnings={
       Array [
-        "warning 1",
+        Object {
+          "dismissable": false,
+          "key": "foo",
+          "message": "warning 1",
+        },
       ]
     }
   />
index 4ad191b493cd7e5ba668d5d6d2a28f3da7524592..fd0e0ab897d714cf36eac286f8a3f766d6b2371a 100644 (file)
@@ -9,10 +9,20 @@ exports[`should render correctly for a branch 1`] = `
       className="header-meta-warnings"
     >
       <ComponentNavWarnings
+        componentKey="my-project"
+        onWarningDismiss={[MockFunction]}
         warnings={
           Array [
-            "ERROR_1",
-            "ERROR_2",
+            Object {
+              "dismissable": false,
+              "key": "foo",
+              "message": "ERROR_1",
+            },
+            Object {
+              "dismissable": false,
+              "key": "foo",
+              "message": "ERROR_2",
+            },
           ]
         }
       />
@@ -52,10 +62,20 @@ exports[`should render correctly for a main project branch 1`] = `
       className="header-meta-warnings"
     >
       <ComponentNavWarnings
+        componentKey="my-project"
+        onWarningDismiss={[MockFunction]}
         warnings={
           Array [
-            "ERROR_1",
-            "ERROR_2",
+            Object {
+              "dismissable": false,
+              "key": "foo",
+              "message": "ERROR_1",
+            },
+            Object {
+              "dismissable": false,
+              "key": "foo",
+              "message": "ERROR_2",
+            },
           ]
         }
       />
@@ -95,10 +115,20 @@ exports[`should render correctly for a portfolio 1`] = `
       className="header-meta-warnings"
     >
       <ComponentNavWarnings
+        componentKey="foo"
+        onWarningDismiss={[MockFunction]}
         warnings={
           Array [
-            "ERROR_1",
-            "ERROR_2",
+            Object {
+              "dismissable": false,
+              "key": "foo",
+              "message": "ERROR_1",
+            },
+            Object {
+              "dismissable": false,
+              "key": "foo",
+              "message": "ERROR_2",
+            },
           ]
         }
       />
@@ -125,10 +155,20 @@ exports[`should render correctly for a pull request 1`] = `
       className="header-meta-warnings"
     >
       <ComponentNavWarnings
+        componentKey="my-project"
+        onWarningDismiss={[MockFunction]}
         warnings={
           Array [
-            "ERROR_1",
-            "ERROR_2",
+            Object {
+              "dismissable": false,
+              "key": "foo",
+              "message": "ERROR_1",
+            },
+            Object {
+              "dismissable": false,
+              "key": "foo",
+              "message": "ERROR_2",
+            },
           ]
         }
       />
index 55423ef6220c82db884afcc66be65d4ed63a1dd4..15783504631ff0750fd7f985089fedd665fcbedb 100644 (file)
@@ -33,23 +33,23 @@ a:focus {
 }
 
 .link-base-color {
-  border-bottom: 1px solid #d0d0d0;
-  color: var(--baseFontColor);
+  border-bottom: 1px solid #d0d0d0 !important;
+  color: var(--baseFontColor) !important;
 }
 
 .link-base-color:hover,
 .link-base-color:active,
 .link-base-color:focus {
-  color: var(--blue);
+  color: var(--blue) !important;
 }
 
 .link-base-color:hover {
-  border-bottom-color: var(--lightBlue);
+  border-bottom-color: var(--lightBlue) !important;
 }
 
 .link-base-color:active,
 .link-base-color:focus {
-  border-bottom-color: var(--lightBlue);
+  border-bottom-color: var(--lightBlue) !important;
 }
 
 .link-no-underline {
index 9810754e5d5ad878b8e9471ba4081bc003176446..ed4fc8682e1e905b7cd6f8d9a5e2758247212794 100644 (file)
@@ -170,7 +170,11 @@ export default class TaskActions extends React.PureComponent<Props, State> {
         {this.state.stacktraceOpen && <Stacktrace onClose={this.closeStacktrace} task={task} />}
 
         {this.state.warningsOpen && (
-          <AnalysisWarningsModal onClose={this.closeWarnings} taskId={task.id} />
+          <AnalysisWarningsModal
+            componentKey={task.componentKey}
+            onClose={this.closeWarnings}
+            taskId={task.id}
+          />
         )}
       </td>
     );
index 9da384c60c4ee6896da1829d254c198eaf0f423a..c47e02b4755dd410736b5c0a60174171a22d61a6 100644 (file)
@@ -161,6 +161,7 @@ exports[`shows stack trace 1`] = `
 
 exports[`shows warnings 1`] = `
 <AnalysisWarningsModal
+  componentKey="foo"
   onClose={[Function]}
   taskId="AXR8jg_0mF2ZsYr8Wzs2"
 />
index 77415ad132f470b533dc92863036ffca3fb96cdd..819fba9f3cdc92dcb9e73f59fa0aba7c212cb12b 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { sanitize } from 'dompurify';
 import * as React from 'react';
-import { ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
+import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
 import Modal from 'sonar-ui-common/components/controls/Modal';
 import WarningIcon from 'sonar-ui-common/components/icons/WarningIcon';
 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
 import { translate } from 'sonar-ui-common/helpers/l10n';
-import { getTask } from '../../api/ce';
+import { dismissAnalysisWarning, getTask } from '../../api/ce';
+import { TaskWarning } from '../../types/tasks';
+import { withCurrentUser } from '../hoc/withCurrentUser';
 
 interface Props {
+  componentKey?: string;
+  currentUser: T.CurrentUser;
   onClose: () => void;
+  onWarningDismiss?: () => void;
   taskId?: string;
-  warnings?: string[];
+  warnings?: TaskWarning[];
 }
 
 interface State {
   loading: boolean;
-  warnings: string[];
+  dismissedWarning?: string;
+  warnings: TaskWarning[];
 }
 
-export default class AnalysisWarningsModal extends React.PureComponent<Props, State> {
+export class AnalysisWarningsModal extends React.PureComponent<Props, State> {
   mounted = false;
 
   constructor(props: Props) {
     super(props);
-    this.state = { loading: !props.warnings, warnings: props.warnings || [] };
+    this.state = {
+      loading: !props.warnings,
+      warnings: props.warnings || []
+    };
   }
 
   componentDidMount() {
@@ -64,42 +74,54 @@ export default class AnalysisWarningsModal extends React.PureComponent<Props, St
     this.mounted = false;
   }
 
-  loadWarnings(taskId: string) {
-    this.setState({ loading: true });
-    getTask(taskId, ['warnings']).then(
-      ({ warnings = [] }) => {
-        if (this.mounted) {
-          this.setState({ loading: false, warnings });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
+  handleDismissMessage = async (messageKey: string) => {
+    const { componentKey } = this.props;
+
+    if (componentKey === undefined) {
+      return;
+    }
+
+    this.setState({ dismissedWarning: messageKey });
+
+    try {
+      await dismissAnalysisWarning(componentKey, messageKey);
+
+      if (this.props.onWarningDismiss) {
+        this.props.onWarningDismiss();
       }
-    );
-  }
+    } catch (e) {
+      // Noop
+    }
+
+    if (this.mounted) {
+      this.setState({ dismissedWarning: undefined });
+    }
+  };
 
-  keepLineBreaks = (warning: string) => {
-    if (warning.includes('\n')) {
-      const lines = warning.split('\n');
-      return (
-        <>
-          {lines.map((line, index) => (
-            <React.Fragment key={index}>
-              {line}
-              {index < lines.length - 1 && <br />}
-            </React.Fragment>
-          ))}
-        </>
-      );
-    } else {
-      return warning;
+  loadWarnings = async (taskId: string) => {
+    this.setState({ loading: true });
+    try {
+      const { warnings = [] } = await getTask(taskId, ['warnings']);
+
+      if (this.mounted) {
+        this.setState({
+          loading: false,
+          warnings: warnings.map(w => ({ key: w, message: w, dismissable: false }))
+        });
+      }
+    } catch (e) {
+      if (this.mounted) {
+        this.setState({ loading: false });
+      }
     }
   };
 
   render() {
+    const { currentUser } = this.props;
+    const { loading, dismissedWarning, warnings } = this.state;
+
     const header = translate('warnings');
+
     return (
       <Modal contentLabel={header} onRequestClose={this.props.onClose}>
         <header className="modal-head">
@@ -107,22 +129,47 @@ export default class AnalysisWarningsModal extends React.PureComponent<Props, St
         </header>
 
         <div className="modal-body modal-container js-analysis-warnings">
-          <DeferredSpinner loading={this.state.loading}>
-            {this.state.warnings.map((warning, index) => (
-              <div className="panel panel-vertical" key={index}>
+          <DeferredSpinner loading={loading}>
+            {warnings.map(({ dismissable, key, message }) => (
+              <div className="panel panel-vertical" key={key}>
                 <WarningIcon className="pull-left spacer-right" />
-                <div className="overflow-hidden markdown">{this.keepLineBreaks(warning)}</div>
+                <div className="overflow-hidden markdown">
+                  <span
+                    // eslint-disable-next-line react/no-danger
+                    dangerouslySetInnerHTML={{
+                      __html: sanitize(message.trim().replace(/\n/g, '<br />'), {
+                        ALLOWED_ATTR: ['target', 'href']
+                      })
+                    }}
+                  />
+
+                  {dismissable && currentUser.isLoggedIn && (
+                    <div className="spacer-top display-flex-inline">
+                      <ButtonLink
+                        className="link-base-color"
+                        disabled={Boolean(dismissedWarning)}
+                        onClick={() => {
+                          this.handleDismissMessage(key);
+                        }}>
+                        {translate('dismiss_permanently')}
+                      </ButtonLink>
+                      {dismissedWarning === key && <i className="spinner spacer-left" />}
+                    </div>
+                  )}
+                </div>
               </div>
             ))}
           </DeferredSpinner>
         </div>
 
         <footer className="modal-foot">
-          <ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
+          <ButtonLink className="js-modal-close" onClick={this.props.onClose}>
             {translate('close')}
-          </ResetButtonLink>
+          </ButtonLink>
         </footer>
       </Modal>
     );
   }
 }
+
+export default withCurrentUser(AnalysisWarningsModal);
index f25fd6978378554810d4d5404c87cffcbd05cec8..408b57509894c88791bff7d1cfb36928315b9a9a 100644 (file)
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { getTask } from '../../../api/ce';
-import AnalysisWarningsModal from '../AnalysisWarningsModal';
+import { dismissAnalysisWarning, getTask } from '../../../api/ce';
+import { mockTaskWarning } from '../../../helpers/mocks/tasks';
+import { mockCurrentUser, mockEvent } from '../../../helpers/testMocks';
+import { AnalysisWarningsModal } from '../AnalysisWarningsModal';
 
 jest.mock('../../../api/ce', () => ({
+  dismissAnalysisWarning: jest.fn().mockResolvedValue(null),
   getTask: jest.fn().mockResolvedValue({
     warnings: ['message foo', 'message-bar', 'multiline message\nsecondline\n  third line']
   })
 }));
 
-beforeEach(() => {
-  (getTask as jest.Mock<any>).mockClear();
+beforeEach(jest.clearAllMocks);
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(shallowRender({ warnings: [mockTaskWarning({ dismissable: true })] })).toMatchSnapshot(
+    'with dismissable warnings'
+  );
+  expect(
+    shallowRender({
+      currentUser: mockCurrentUser({ isLoggedIn: false }),
+      warnings: [mockTaskWarning({ dismissable: true })]
+    })
+  ).toMatchSnapshot('do not show dismissable links for anonymous');
+});
+
+it('should not fetch task warnings if it does not have to', () => {
+  shallowRender();
+  expect(getTask).not.toBeCalled();
 });
 
-it('should fetch warnings and render', async () => {
-  const wrapper = shallow(<AnalysisWarningsModal onClose={jest.fn()} taskId="abcd1234" />);
+it('should fetch task warnings if it has to', async () => {
+  const wrapper = shallowRender({ taskId: 'abcd1234', warnings: undefined });
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
   expect(getTask).toBeCalledWith('abcd1234', ['warnings']);
 });
 
-it('should render warnings without fetch', () => {
-  const wrapper = shallow(
-    <AnalysisWarningsModal onClose={jest.fn()} warnings={['warning 1', 'warning 2']} />
-  );
-  expect(wrapper).toMatchSnapshot();
+it('should correctly handle dismissing warnings', () => {
+  return new Promise(resolve => {
+    const onWarningDismiss = jest.fn();
+    const wrapper = shallowRender({
+      componentKey: 'foo',
+      onWarningDismiss,
+      warnings: [mockTaskWarning({ key: 'bar', dismissable: true })]
+    });
+
+    const click = wrapper.find('ButtonLink.link-base-color').props().onClick;
+    if (click) {
+      click(mockEvent());
+
+      waitAndUpdate(wrapper).then(
+        () => {
+          expect(dismissAnalysisWarning).toBeCalledWith('foo', 'bar');
+          expect(onWarningDismiss).toBeCalled();
+          resolve();
+        },
+        () => {}
+      );
+    }
+  });
+});
+
+it('should correctly handle updates', async () => {
+  const wrapper = shallowRender();
+
+  await waitAndUpdate(wrapper);
+  expect(getTask).not.toBeCalled();
+
+  wrapper.setProps({ taskId: '1', warnings: undefined });
+  await waitAndUpdate(wrapper);
+  expect(getTask).toBeCalled();
+
+  (getTask as jest.Mock).mockClear();
+  wrapper.setProps({ taskId: undefined, warnings: [mockTaskWarning()] });
   expect(getTask).not.toBeCalled();
 });
+
+function shallowRender(props: Partial<AnalysisWarningsModal['props']> = {}) {
+  return shallow<AnalysisWarningsModal>(
+    <AnalysisWarningsModal
+      currentUser={mockCurrentUser({ isLoggedIn: true })}
+      onClose={jest.fn()}
+      warnings={[
+        mockTaskWarning({ message: 'warning 1' }),
+        mockTaskWarning({ message: 'warning 2' })
+      ]}
+      {...props}
+    />
+  );
+}
index bec58ec82a967b88dae9f5ecc1eca4513d601985..b703954b0eae133d0908893848e89b80d9366c7a 100644 (file)
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should fetch warnings and render 1`] = `
+exports[`should fetch task warnings if it has to 1`] = `
 <Modal
   contentLabel="warnings"
   onRequestClose={[MockFunction]}
@@ -20,7 +20,7 @@ exports[`should fetch warnings and render 1`] = `
     >
       <div
         className="panel panel-vertical"
-        key="0"
+        key="message foo"
       >
         <WarningIcon
           className="pull-left spacer-right"
@@ -28,12 +28,18 @@ exports[`should fetch warnings and render 1`] = `
         <div
           className="overflow-hidden markdown"
         >
-          message foo
+          <span
+            dangerouslySetInnerHTML={
+              Object {
+                "__html": "message foo",
+              }
+            }
+          />
         </div>
       </div>
       <div
         className="panel panel-vertical"
-        key="1"
+        key="message-bar"
       >
         <WarningIcon
           className="pull-left spacer-right"
@@ -41,12 +47,20 @@ exports[`should fetch warnings and render 1`] = `
         <div
           className="overflow-hidden markdown"
         >
-          message-bar
+          <span
+            dangerouslySetInnerHTML={
+              Object {
+                "__html": "message-bar",
+              }
+            }
+          />
         </div>
       </div>
       <div
         className="panel panel-vertical"
-        key="2"
+        key="multiline message
+secondline
+  third line"
       >
         <WarningIcon
           className="pull-left spacer-right"
@@ -54,11 +68,13 @@ exports[`should fetch warnings and render 1`] = `
         <div
           className="overflow-hidden markdown"
         >
-          multiline message
-          <br />
-          secondline
-          <br />
-            third line
+          <span
+            dangerouslySetInnerHTML={
+              Object {
+                "__html": "multiline message<br>secondline<br>  third line",
+              }
+            }
+          />
         </div>
       </div>
     </DeferredSpinner>
@@ -66,17 +82,17 @@ exports[`should fetch warnings and render 1`] = `
   <footer
     className="modal-foot"
   >
-    <ResetButtonLink
+    <ButtonLink
       className="js-modal-close"
       onClick={[MockFunction]}
     >
       close
-    </ResetButtonLink>
+    </ButtonLink>
   </footer>
 </Modal>
 `;
 
-exports[`should render warnings without fetch 1`] = `
+exports[`should render correctly: default 1`] = `
 <Modal
   contentLabel="warnings"
   onRequestClose={[MockFunction]}
@@ -96,7 +112,7 @@ exports[`should render warnings without fetch 1`] = `
     >
       <div
         className="panel panel-vertical"
-        key="0"
+        key="foo"
       >
         <WarningIcon
           className="pull-left spacer-right"
@@ -104,12 +120,18 @@ exports[`should render warnings without fetch 1`] = `
         <div
           className="overflow-hidden markdown"
         >
-          warning 1
+          <span
+            dangerouslySetInnerHTML={
+              Object {
+                "__html": "warning 1",
+              }
+            }
+          />
         </div>
       </div>
       <div
         className="panel panel-vertical"
-        key="1"
+        key="foo"
       >
         <WarningIcon
           className="pull-left spacer-right"
@@ -117,7 +139,13 @@ exports[`should render warnings without fetch 1`] = `
         <div
           className="overflow-hidden markdown"
         >
-          warning 2
+          <span
+            dangerouslySetInnerHTML={
+              Object {
+                "__html": "warning 2",
+              }
+            }
+          />
         </div>
       </div>
     </DeferredSpinner>
@@ -125,12 +153,127 @@ exports[`should render warnings without fetch 1`] = `
   <footer
     className="modal-foot"
   >
-    <ResetButtonLink
+    <ButtonLink
       className="js-modal-close"
       onClick={[MockFunction]}
     >
       close
-    </ResetButtonLink>
+    </ButtonLink>
+  </footer>
+</Modal>
+`;
+
+exports[`should render correctly: do not show dismissable links for anonymous 1`] = `
+<Modal
+  contentLabel="warnings"
+  onRequestClose={[MockFunction]}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      warnings
+    </h2>
+  </header>
+  <div
+    className="modal-body modal-container js-analysis-warnings"
+  >
+    <DeferredSpinner
+      loading={false}
+    >
+      <div
+        className="panel panel-vertical"
+        key="foo"
+      >
+        <WarningIcon
+          className="pull-left spacer-right"
+        />
+        <div
+          className="overflow-hidden markdown"
+        >
+          <span
+            dangerouslySetInnerHTML={
+              Object {
+                "__html": "Lorem ipsum",
+              }
+            }
+          />
+        </div>
+      </div>
+    </DeferredSpinner>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <ButtonLink
+      className="js-modal-close"
+      onClick={[MockFunction]}
+    >
+      close
+    </ButtonLink>
+  </footer>
+</Modal>
+`;
+
+exports[`should render correctly: with dismissable warnings 1`] = `
+<Modal
+  contentLabel="warnings"
+  onRequestClose={[MockFunction]}
+>
+  <header
+    className="modal-head"
+  >
+    <h2>
+      warnings
+    </h2>
+  </header>
+  <div
+    className="modal-body modal-container js-analysis-warnings"
+  >
+    <DeferredSpinner
+      loading={false}
+    >
+      <div
+        className="panel panel-vertical"
+        key="foo"
+      >
+        <WarningIcon
+          className="pull-left spacer-right"
+        />
+        <div
+          className="overflow-hidden markdown"
+        >
+          <span
+            dangerouslySetInnerHTML={
+              Object {
+                "__html": "Lorem ipsum",
+              }
+            }
+          />
+          <div
+            className="spacer-top display-flex-inline"
+          >
+            <ButtonLink
+              className="link-base-color"
+              disabled={false}
+              onClick={[Function]}
+            >
+              dismiss_permanently
+            </ButtonLink>
+          </div>
+        </div>
+      </div>
+    </DeferredSpinner>
+  </div>
+  <footer
+    className="modal-foot"
+  >
+    <ButtonLink
+      className="js-modal-close"
+      onClick={[MockFunction]}
+    >
+      close
+    </ButtonLink>
   </footer>
 </Modal>
 `;
index db0c6b08569eac62f1a421789ad1b06656f98817..f751e198d9cee230dcf528bb9f92e568c1da9673 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { ComponentQualifier } from '../../types/component';
-import { Task, TaskStatuses, TaskTypes } from '../../types/tasks';
+import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks';
 
 export function mockTask(overrides: Partial<Task> = {}): Task {
   return {
@@ -34,3 +34,12 @@ export function mockTask(overrides: Partial<Task> = {}): Task {
     ...overrides
   };
 }
+
+export function mockTaskWarning(overrides: Partial<TaskWarning> = {}): TaskWarning {
+  return {
+    key: 'foo',
+    message: 'Lorem ipsum',
+    dismissable: false,
+    ...overrides
+  };
+}
index 8efac5a7f2b75e840b345030354fcada86918fa0..4eab7ff6357cf1ecf18e12b237cdfac1728b01ac 100644 (file)
@@ -58,3 +58,9 @@ export interface Task {
   warningCount?: number;
   warnings?: string[];
 }
+
+export interface TaskWarning {
+  key: string;
+  message: string;
+  dismissable: boolean;
+}
index 7fa7d6f3af5bbe63c1fecb379efef0b329b17915..e1c614546711ac024325c8fefd25eb14dbe146c2 100644 (file)
@@ -63,6 +63,7 @@ description=Description
 directories=Directories
 directory=Directory
 dismiss=Dismiss
+dismiss_permanently=Dismiss permanently
 display=Display
 download_verb=Download
 duplications=Duplications