Browse Source

SONAR-13862 Allow analysis messages to be permanently dismissed

tags/8.5.0.37579
Wouter Admiraal 3 years ago
parent
commit
27282ea049
21 changed files with 1358 additions and 148 deletions
  1. 6
    2
      server/sonar-web/src/main/js/api/ce.ts
  2. 10
    2
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  3. 17
    1
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  4. 11
    5
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  5. 10
    2
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx
  6. 8
    2
      server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx
  7. 86
    17
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
  8. 8
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx
  9. 3
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx
  10. 789
    27
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
  11. 7
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap
  12. 48
    8
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap
  13. 5
    5
      server/sonar-web/src/main/js/app/styles/init/links.css
  14. 5
    1
      server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
  15. 1
    0
      server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap
  16. 88
    41
      server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx
  17. 76
    11
      server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
  18. 163
    20
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap
  19. 10
    1
      server/sonar-web/src/main/js/helpers/mocks/tasks.ts
  20. 6
    0
      server/sonar-web/src/main/js/types/tasks.ts
  21. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 6
- 2
server/sonar-web/src/main/js/api/ce.ts View 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);
}

+ 10
- 2
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx View 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}
/>
)}

+ 17
- 1
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx View 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

+ 11
- 5
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx View 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}

+ 10
- 2
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavWarnings.tsx View 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}
/>
)}
</>
);

+ 8
- 2
server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx View 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 && (

+ 86
- 17
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx View File

@@ -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();
});
}

+ 8
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavWarnings-test.tsx View File

@@ -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();
});

+ 3
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx View 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}
/>
);

+ 789
- 27
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap View 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]}

+ 7
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavWarnings-test.tsx.snap View 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",
},
]
}
/>

+ 48
- 8
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap View 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",
},
]
}
/>

+ 5
- 5
server/sonar-web/src/main/js/app/styles/init/links.css View 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 {

+ 5
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx View 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>
);

+ 1
- 0
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskActions-test.tsx.snap View File

@@ -161,6 +161,7 @@ exports[`shows stack trace 1`] = `

exports[`shows warnings 1`] = `
<AnalysisWarningsModal
componentKey="foo"
onClose={[Function]}
taskId="AXR8jg_0mF2ZsYr8Wzs2"
/>

+ 88
- 41
server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx View File

@@ -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);

+ 76
- 11
server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx View File

@@ -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}
/>
);
}

+ 163
- 20
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap View 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>
`;

+ 10
- 1
server/sonar-web/src/main/js/helpers/mocks/tasks.ts View 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
};
}

+ 6
- 0
server/sonar-web/src/main/js/types/tasks.ts View File

@@ -58,3 +58,9 @@ export interface Task {
warningCount?: number;
warnings?: string[];
}

export interface TaskWarning {
key: string;
message: string;
dismissable: boolean;
}

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -63,6 +63,7 @@ description=Description
directories=Directories
directory=Directory
dismiss=Dismiss
dismiss_permanently=Dismiss permanently
display=Display
download_verb=Download
duplications=Duplications

Loading…
Cancel
Save