From 27282ea04940d30eb33c20a02989a79d88dec4a0 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Mon, 14 Sep 2020 17:00:12 +0200 Subject: [PATCH] SONAR-13862 Allow analysis messages to be permanently dismissed --- server/sonar-web/src/main/js/api/ce.ts | 8 +- .../js/app/components/ComponentContainer.tsx | 12 +- .../__tests__/ComponentContainer-test.tsx | 18 +- .../components/nav/component/ComponentNav.tsx | 16 +- .../nav/component/ComponentNavWarnings.tsx | 12 +- .../components/nav/component/HeaderMeta.tsx | 10 +- .../component/__tests__/ComponentNav-test.tsx | 103 ++- .../__tests__/ComponentNavWarnings-test.tsx | 9 +- .../component/__tests__/HeaderMeta-test.tsx | 4 +- .../__snapshots__/ComponentNav-test.tsx.snap | 816 +++++++++++++++++- .../ComponentNavWarnings-test.tsx.snap | 8 +- .../__snapshots__/HeaderMeta-test.tsx.snap | 56 +- .../src/main/js/app/styles/init/links.css | 10 +- .../components/TaskActions.tsx | 6 +- .../__snapshots__/TaskActions-test.tsx.snap | 1 + .../common/AnalysisWarningsModal.tsx | 129 ++- .../__tests__/AnalysisWarningsModal-test.tsx | 87 +- .../AnalysisWarningsModal-test.tsx.snap | 183 +++- .../src/main/js/helpers/mocks/tasks.ts | 11 +- server/sonar-web/src/main/js/types/tasks.ts | 6 + .../resources/org/sonar/l10n/core.properties | 1 + 21 files changed, 1358 insertions(+), 148 deletions(-) diff --git a/server/sonar-web/src/main/js/api/ce.ts b/server/sonar-web/src/main/js/api/ce.ts index e2b9a1f846c..35f43ef9e1d 100644 --- a/server/sonar-web/src/main/js/api/ce.ts +++ b/server/sonar-web/src/main/js/api/ce.ts @@ -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 { export function getIndexationStatus(): Promise { 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); +} diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index dc540a9b5b4..02ac49bfea0 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -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 { } }; + 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 { isInProgress={isInProgress} isPending={isPending} onComponentChange={this.handleComponentChange} + onWarningDismiss={this.handleWarningDismiss} warnings={this.state.warnings} /> )} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index 0a23c044138..36bebb2c835 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -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).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 = {}) { return shallow( ) => 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} /> - + 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 {this.state.modal && ( - + )} ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx index 5706d62927c..4d8d4763a05 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx @@ -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) {
{warnings.length > 0 && ( - + )} {component.analysisDate && ( diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx index 205f7bf64ad..c14e8a43ebb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx @@ -19,27 +19,96 @@ */ 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 = {}) { + return shallow( ); - expect(wrapper).toMatchSnapshot(); -}); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx index 7bedb07be3c..431bdf14d6c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx @@ -19,10 +19,17 @@ */ 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(); + const wrapper = shallow( + + ); wrapper.setState({ modal: true }); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx index b918bdacb38..a76e8cd6f88 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx @@ -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 = {}) { 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} /> ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap index e311796c90b..bf8b1225b71 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap @@ -1,6 +1,354 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders 1`] = ` +exports[`renders correctly: default 1`] = ` + +
+ + +
+ + + + +
+`; + +exports[`renders correctly: has failed notification 1`] = ` + + } +> +
+ + +
+ + + + +
+`; + +exports[`renders correctly: has in progress notification 1`] = ` } > @@ -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 []} />
@@ -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]} + /> + + + + +`; + +exports[`renders correctly: has pending notification 1`] = ` + + } +> +
+ + +
+ @@ -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]} + /> + +
+`; + +exports[`renders correctly: has warnings 1`] = ` + +
+ + +
+ + + diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap index 4ad191b493c..fd0e0ab897d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap @@ -9,10 +9,20 @@ exports[`should render correctly for a branch 1`] = ` className="header-meta-warnings" > @@ -52,10 +62,20 @@ exports[`should render correctly for a main project branch 1`] = ` className="header-meta-warnings" > @@ -95,10 +115,20 @@ exports[`should render correctly for a portfolio 1`] = ` className="header-meta-warnings" > @@ -125,10 +155,20 @@ exports[`should render correctly for a pull request 1`] = ` className="header-meta-warnings" > diff --git a/server/sonar-web/src/main/js/app/styles/init/links.css b/server/sonar-web/src/main/js/app/styles/init/links.css index 55423ef6220..15783504631 100644 --- a/server/sonar-web/src/main/js/app/styles/init/links.css +++ b/server/sonar-web/src/main/js/app/styles/init/links.css @@ -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 { diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx index 9810754e5d5..ed4fc8682e1 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx @@ -170,7 +170,11 @@ export default class TaskActions extends React.PureComponent { {this.state.stacktraceOpen && } {this.state.warningsOpen && ( - + )} ); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap index 9da384c60c4..c47e02b4755 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap @@ -161,6 +161,7 @@ exports[`shows stack trace 1`] = ` exports[`shows warnings 1`] = ` diff --git a/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx index 77415ad132f..819fba9f3cd 100644 --- a/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx +++ b/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx @@ -17,31 +17,41 @@ * 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 { +export class AnalysisWarningsModal extends React.PureComponent { 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 { - 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) => ( - - {line} - {index < lines.length - 1 &&
} -
- ))} - - ); - } 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 (
@@ -107,22 +129,47 @@ export default class AnalysisWarningsModal extends React.PureComponent
- - {this.state.warnings.map((warning, index) => ( -
+ + {warnings.map(({ dismissable, key, message }) => ( +
-
{this.keepLineBreaks(warning)}
+
+ '), { + ALLOWED_ATTR: ['target', 'href'] + }) + }} + /> + + {dismissable && currentUser.isLoggedIn && ( +
+ { + this.handleDismissMessage(key); + }}> + {translate('dismiss_permanently')} + + {dismissedWarning === key && } +
+ )} +
))}
- + {translate('close')} - +
); } } + +export default withCurrentUser(AnalysisWarningsModal); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx index f25fd697837..408b5750989 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx +++ b/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx @@ -20,30 +20,95 @@ 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).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(); +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( - - ); - 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 = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap index bec58ec82a9..b703954b0ea 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap @@ -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`] = `
- message foo +
- message-bar +
- multiline message -
- secondline -
- third line + secondline
third line", + } + } + />
@@ -66,17 +82,17 @@ exports[`should fetch warnings and render 1`] = `
- close - +
`; -exports[`should render warnings without fetch 1`] = ` +exports[`should render correctly: default 1`] = `
- warning 1 +
- warning 2 +
@@ -125,12 +153,127 @@ exports[`should render warnings without fetch 1`] = `
- close - + +
+
+`; + +exports[`should render correctly: do not show dismissable links for anonymous 1`] = ` + +
+

+ warnings +

+
+
+ +
+ +
+ +
+
+
+
+
+ + close + +
+
+`; + +exports[`should render correctly: with dismissable warnings 1`] = ` + +
+

+ warnings +

+
+
+ +
+ +
+ +
+ + dismiss_permanently + +
+
+
+
+
+
+ + close +
`; diff --git a/server/sonar-web/src/main/js/helpers/mocks/tasks.ts b/server/sonar-web/src/main/js/helpers/mocks/tasks.ts index db0c6b08569..f751e198d9c 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/tasks.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/tasks.ts @@ -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 { return { @@ -34,3 +34,12 @@ export function mockTask(overrides: Partial = {}): Task { ...overrides }; } + +export function mockTaskWarning(overrides: Partial = {}): TaskWarning { + return { + key: 'foo', + message: 'Lorem ipsum', + dismissable: false, + ...overrides + }; +} diff --git a/server/sonar-web/src/main/js/types/tasks.ts b/server/sonar-web/src/main/js/types/tasks.ts index 8efac5a7f2b..4eab7ff6357 100644 --- a/server/sonar-web/src/main/js/types/tasks.ts +++ b/server/sonar-web/src/main/js/types/tasks.ts @@ -58,3 +58,9 @@ export interface Task { warningCount?: number; warnings?: string[]; } + +export interface TaskWarning { + key: string; + message: string; + dismissable: boolean; +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 7fa7d6f3af5..e1c61454671 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -63,6 +63,7 @@ description=Description directories=Directories directory=Directory dismiss=Dismiss +dismiss_permanently=Dismiss permanently display=Display download_verb=Download duplications=Duplications -- 2.39.5