@@ -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); | |||
} |
@@ -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} | |||
/> | |||
)} |
@@ -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 |
@@ -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} |
@@ -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} | |||
/> | |||
)} | |||
</> | |||
); |
@@ -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 && ( |
@@ -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<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(); | |||
}); | |||
} |
@@ -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(<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(); | |||
}); |
@@ -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} | |||
/> | |||
); |
@@ -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]} |
@@ -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", | |||
}, | |||
] | |||
} | |||
/> |
@@ -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", | |||
}, | |||
] | |||
} | |||
/> |
@@ -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 { |
@@ -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> | |||
); |
@@ -161,6 +161,7 @@ exports[`shows stack trace 1`] = ` | |||
exports[`shows warnings 1`] = ` | |||
<AnalysisWarningsModal | |||
componentKey="foo" | |||
onClose={[Function]} | |||
taskId="AXR8jg_0mF2ZsYr8Wzs2" | |||
/> |
@@ -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<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); |
@@ -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<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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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 | |||
}; | |||
} |
@@ -58,3 +58,9 @@ export interface Task { | |||
warningCount?: number; | |||
warnings?: string[]; | |||
} | |||
export interface TaskWarning { | |||
key: string; | |||
message: string; | |||
dismissable: boolean; | |||
} |
@@ -63,6 +63,7 @@ description=Description | |||
directories=Directories | |||
directory=Directory | |||
dismiss=Dismiss | |||
dismiss_permanently=Dismiss permanently | |||
display=Display | |||
download_verb=Download | |||
duplications=Duplications |