return getJSON('/api/ce/activity_status', data);
}
-export function getTask(id: string, additionalFields?: string[]): Promise<any> {
+export function getTask(id: string, additionalFields?: string[]): Promise<Task> {
return getJSON('/api/ce/task', { id, additionalFields }).then(r => r.task);
}
branchLike={currentBranchLike}
branchMeasures={this.props.branchMeasures}
component={component}
+ currentTask={currentTask}
/>
</div>
<ComponentNavMenu
*/
import * as React from 'react';
import { connect } from 'react-redux';
+import ComponentNavWarnings from './ComponentNavWarnings';
import {
BranchLike,
Component,
isLoggedIn,
HomePageType,
HomePage,
- Measure
+ Measure,
+ Task
} from '../../../types';
import BranchMeasures from '../../../../components/common/BranchMeasures';
import BranchStatus from '../../../../components/common/BranchStatus';
branchLike?: BranchLike;
branchMeasures?: Measure[];
component: Component;
+ currentTask?: Task;
}
-export function ComponentNavMeta({ branchLike, branchMeasures, component, currentUser }: Props) {
+export function ComponentNavMeta({ branchLike, branchMeasures, component, currentTask, currentUser }: Props) {
const mainBranch = !branchLike || isMainBranch(branchLike);
const longBranch = isLongLivingBranch(branchLike);
const currentPage = getCurrentPage(component, branchLike);
return (
<div className="navbar-context-meta">
+ {currentTask &&
+ Boolean(currentTask.warningCount) && <ComponentNavWarnings task={currentTask} />}
{component.analysisDate && (
<div className="spacer-left text-ellipsis">
<DateTimeFormatter date={component.analysisDate} />
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { lazyLoad } from '../../../../components/lazyLoad';
+import WarningIcon from '../../../../components/icons-components/WarningIcon';
+import { Task } from '../../../types';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+
+const AnalysisWarningsModal = lazyLoad(() =>
+ import('../../../../components/common/AnalysisWarningsModal')
+);
+
+interface Props {
+ task: Pick<Task, 'id' | 'warningCount'>;
+}
+
+interface State {
+ modal: boolean;
+}
+
+export default class ComponentNavWarnings extends React.PureComponent<Props, State> {
+ state: State = { modal: false };
+
+ handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.setState({ modal: true });
+ };
+
+ handleCloseModal = () => {
+ this.setState({ modal: false });
+ };
+
+ render() {
+ return (
+ <>
+ <div className="badge badge-focus badge-medium display-inline-flex-center js-component-analysis-warnings">
+ <WarningIcon className="spacer-right" />
+ <FormattedMessage
+ defaultMessage={translate('component_navigation.last_analsys_had_warnings')}
+ id="component_navigation.last_analsys_had_warnings"
+ values={{
+ warnings: (
+ <a href="#" onClick={this.handleClick}>
+ {translateWithParameters(
+ 'component_navigation.x_warnings',
+ String(this.props.task.warningCount)
+ )}
+ </a>
+ )
+ }}
+ />
+ </div>
+ {this.state.modal && (
+ <AnalysisWarningsModal onClose={this.handleCloseModal} taskId={this.props.task.id} />
+ )}
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import ComponentNavWarnings from '../ComponentNavWarnings';
+
+it('should render', () => {
+ const wrapper = shallow(<ComponentNavWarnings task={{ id: 'abcd1234' }} />);
+ wrapper.setState({ modal: true });
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<React.Fragment>
+ <div
+ className="badge badge-focus badge-medium display-inline-flex-center js-component-analysis-warnings"
+ >
+ <WarningIcon
+ className="spacer-right"
+ />
+ <FormattedMessage
+ defaultMessage="component_navigation.last_analsys_had_warnings"
+ id="component_navigation.last_analsys_had_warnings"
+ values={
+ Object {
+ "warnings": <a
+ href="#"
+ onClick={[Function]}
+ >
+ component_navigation.x_warnings.undefined
+ </a>,
+ }
+ }
+ />
+ </div>
+ <LazyLoader
+ onClose={[Function]}
+ taskId="abcd1234"
+ />
+</React.Fragment>
+`;
border-radius: 50px;
}
+.badge-medium {
+ height: var(--controlHeight);
+ line-height: calc(var(--controlHeight));
+ letter-spacing: 0.01em;
+}
+
.list-group-item > .badge,
.list-group-item-heading > .badge {
float: right;
snippetFontColor: '#f0f0f0',
+ // alerts
+ warningIconColor: '#e2bf41',
+
// sizes
grid,
gridSize: `${grid}px`,
componentName?: string;
componentQualifier?: string;
errorMessage?: string;
+ errorStacktrace?: string;
errorType?: string;
executedAt?: string;
executionTimeMs?: number;
organization: string;
pullRequest?: string;
pullRequestTitle?: string;
+ scannerContext?: string;
startedAt?: string;
status: string;
submittedAt: string;
submitterLogin?: string;
type: string;
+ warningCount?: number;
+ warnings?: string[];
}
export interface TestCase {
import { translate, translateWithParameters } from '../../../helpers/l10n';
import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
import { Task } from '../../../app/types';
+import { lazyLoad } from '../../../components/lazyLoad';
+
+const AnalysisWarningsModal = lazyLoad(
+ () => import('../../../components/common/AnalysisWarningsModal'),
+ 'AnalysisWarningsModal'
+);
interface Props {
component?: {};
interface State {
scannerContextOpen: boolean;
stacktraceOpen: boolean;
+ warningsOpen: boolean;
}
export default class TaskActions extends React.PureComponent<Props, State> {
state: State = {
scannerContextOpen: false,
- stacktraceOpen: false
+ stacktraceOpen: false,
+ warningsOpen: false
};
handleFilterClick = () => {
this.setState({ scannerContextOpen: true });
};
- closeScannerContext = () => this.setState({ scannerContextOpen: false });
+ closeScannerContext = () => {
+ this.setState({ scannerContextOpen: false });
+ };
handleShowStacktraceClick = () => {
this.setState({ stacktraceOpen: true });
};
- closeStacktrace = () => this.setState({ stacktraceOpen: false });
+ closeStacktrace = () => {
+ this.setState({ stacktraceOpen: false });
+ };
+
+ handleShowWarningsClick = () => {
+ this.setState({ warningsOpen: true });
+ };
+
+ closeWarnings = () => {
+ this.setState({ warningsOpen: false });
+ };
render() {
const { component, task } = this.props;
const canFilter = component === undefined;
const canCancel = task.status === STATUSES.PENDING;
const canShowStacktrace = task.errorMessage !== undefined;
- const hasActions = canFilter || canCancel || task.hasScannerContext || canShowStacktrace;
+ const canShowWarnings = task.warningCount !== undefined && task.warningCount > 0;
+ const hasActions =
+ canFilter || canCancel || task.hasScannerContext || canShowStacktrace || canShowWarnings;
if (!hasActions) {
return <td> </td>;
{translate('background_tasks.show_stacktrace')}
</ActionsDropdownItem>
)}
+ {canShowWarnings && (
+ <ActionsDropdownItem
+ className="js-task-show-warnings"
+ onClick={this.handleShowWarningsClick}>
+ {translate('background_tasks.show_warnings')}
+ </ActionsDropdownItem>
+ )}
</ActionsDropdown>
{this.state.scannerContextOpen && (
)}
{this.state.stacktraceOpen && <Stacktrace onClose={this.closeStacktrace} task={task} />}
+
+ {this.state.warningsOpen && (
+ <AnalysisWarningsModal onClose={this.closeWarnings} taskId={task.id} />
+ )}
</td>
);
}
import { shallow } from 'enzyme';
import TaskActions from '../TaskActions';
import { click } from '../../../../helpers/testUtils';
+import { Task } from '../../../../app/types';
it('renders', () => {
expect(shallowRender()).toMatchSnapshot();
expect(wrapper.find('ScannerContext').exists()).toBeFalsy();
});
-function shallowRender(fields?: any, props?: any) {
+it('shows warnings', () => {
+ const wrapper = shallowRender({ warningCount: 2 });
+ click(wrapper.find('.js-task-show-warnings'));
+ expect(wrapper.find('AnalysisWarningsModal')).toMatchSnapshot();
+ wrapper.find('AnalysisWarningsModal').prop<Function>('onClose')();
+ wrapper.update();
+ expect(wrapper.find('AnalysisWarningsModal').exists()).toBeFalsy();
+});
+
+function shallowRender(fields?: Partial<Task>, props?: Partial<TaskActions['props']>) {
return shallow(
<TaskActions
onCancelTask={jest.fn()}
componentName: 'foo',
status: 'PENDING',
id: '123',
+ organization: 'org',
submittedAt: '2017-01-01',
type: 'REPORT',
...fields
"componentName": "foo",
"hasScannerContext": true,
"id": "123",
+ "organization": "org",
"status": "PENDING",
"submittedAt": "2017-01-01",
"type": "REPORT",
"componentName": "foo",
"errorMessage": "error!",
"id": "123",
+ "organization": "org",
"status": "PENDING",
"submittedAt": "2017-01-01",
"type": "REPORT",
}
/>
`;
+
+exports[`shows warnings 1`] = `
+<AnalysisWarningsModal
+ onClose={[Function]}
+ taskId="123"
+/>
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import DeferredSpinner from './DeferredSpinner';
+import Modal from '../controls/Modal';
+import { ResetButtonLink } from '../ui/buttons';
+import { translate } from '../../helpers/l10n';
+import WarningIcon from '../icons-components/WarningIcon';
+import { getTask } from '../../api/ce';
+
+interface Props {
+ onClose: () => void;
+ taskId: string;
+}
+
+interface State {
+ loading: boolean;
+ warnings: string[];
+}
+
+export default class AnalysisWarningsModal extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {
+ loading: true,
+ warnings: []
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.loadWarnings();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.taskId !== this.props.taskId) {
+ this.loadWarnings();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ loadWarnings() {
+ this.setState({ loading: true });
+ getTask(this.props.taskId, ['warnings']).then(
+ ({ warnings = [] }) => {
+ if (this.mounted) {
+ this.setState({ loading: false, warnings });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ }
+
+ render() {
+ const header = translate('warnings');
+ return (
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+
+ <div className="modal-body js-analysis-warnings">
+ <DeferredSpinner loading={this.state.loading}>
+ {this.state.warnings.map((warning, index) => (
+ <div className="panel panel-vertical" key={index}>
+ <WarningIcon className="pull-left spacer-right" />
+ <div className="overflow-hidden markdown">{warning}</div>
+ </div>
+ ))}
+ </DeferredSpinner>
+ </div>
+
+ <footer className="modal-foot">
+ <ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
+ {translate('close')}
+ </ResetButtonLink>
+ </footer>
+ </Modal>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import AnalysisWarningsModal from '../AnalysisWarningsModal';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+import { getTask } from '../../../api/ce';
+
+jest.mock('../../../api/ce', () => ({
+ getTask: jest.fn().mockResolvedValue({ warnings: ['message foo', 'message-bar'] })
+}));
+
+it('should fetch warnings and render', async () => {
+ const wrapper = shallow(<AnalysisWarningsModal onClose={jest.fn()} taskId="abcd1234" />);
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+ expect(getTask).toBeCalledWith('abcd1234', ['warnings']);
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should fetch warnings and render 1`] = `
+<Modal
+ contentLabel="warnings"
+ onRequestClose={[MockFunction]}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ warnings
+ </h2>
+ </header>
+ <div
+ className="modal-body js-analysis-warnings"
+ >
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <div
+ className="panel panel-vertical"
+ key="0"
+ >
+ <WarningIcon
+ className="pull-left spacer-right"
+ />
+ <div
+ className="overflow-hidden markdown"
+ >
+ message foo
+ </div>
+ </div>
+ <div
+ className="panel panel-vertical"
+ key="1"
+ >
+ <WarningIcon
+ className="pull-left spacer-right"
+ />
+ <div
+ className="overflow-hidden markdown"
+ >
+ message-bar
+ </div>
+ </div>
+ </DeferredSpinner>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <ResetButtonLink
+ className="js-modal-close"
+ onClick={[MockFunction]}
+ >
+ close
+ </ResetButtonLink>
+ </footer>
+</Modal>
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+import * as theme from '../../app/theme';
+
+export default function WarningIcon({ className, fill = theme.warningIconColor, size }: IconProps) {
+ return (
+ <Icon className={className} size={size}>
+ <path
+ d="M9 12.242v-1.484c0-.14-.11-.258-.25-.258h-1.5c-.14 0-.25.117-.25.258v1.484c0 .14.11.258.25.258h1.5c.14 0 .25-.117.25-.258zM8.984 9.32l.141-3.586a.189.189 0 0 0-.078-.148C9 5.546 8.93 5.5 8.859 5.5H7.141c-.07 0-.141.047-.188.086-.055.039-.078.117-.078.164l.133 3.57c0 .102.117.18.265.18H8.72c.14 0 .258-.078.265-.18zm-.109-7.297l6 11A1 1 0 0 1 14 14.5H2a1 1 0 0 1-.875-1.477l6-11a.994.994 0 0 1 1.75 0z"
+ style={{ fill }}
+ />
+ </Icon>
+ );
+}
views=Views
violations=Violations
visibility=Visibility
+warnings=Warnings
with=With
worst=Worst
yes=Yes
component_navigation.status.in_progress.admin=The analysis is in progress. More details available on the {url} page.
component_navigation.status.last_blocked_due_to_bad_license=Last analysis blocked due to an invalid license, which has since been corrected. Please reanalyze this project.
+component_navigation.last_analsys_had_warnings=Last analysis had {warnings}
+component_navigation.x_warnings={0} warnings
+
background_task.status.ALL=All
background_task.status.PENDING=Pending
background_task.status.IN_PROGRESS=In Progress
background_tasks.scanner_context=Scanner Context
background_tasks.show_scanner_context=Show Scanner Context
background_tasks.show_stacktrace=Show Error Details
+background_tasks.show_warnings=Show Warnings
background_tasks.error_message=Error Message
background_tasks.error_stacktrace=Error Details
background_tasks.pending=pending