Browse Source

SONAR-10763 display quality gate events for applications (#910)

tags/7.5
Stas Vilchik 5 years ago
parent
commit
5a5c9bfb9f

+ 5
- 0
server/sonar-web/src/main/js/app/types.ts View File

@@ -57,6 +57,11 @@ export interface AnalysisEvent {
description?: string;
key: string;
name: string;
qualityGate?: {
failing: Array<{ branch: string; key: string; name: string }>;
status: string;
stillFailing: boolean;
};
}

export interface AppState {

+ 21
- 1
server/sonar-web/src/main/js/apps/overview/events/Event.tsx View File

@@ -18,8 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { FormattedMessage } from 'react-intl';
import { AnalysisEvent } from '../../../app/types';
import { isRichQualityGateEvent } from '../../projectActivity/components/RichQualityGateEventInner';
import Level from '../../../components/ui/Level';
import { translate } from '../../../helpers/l10n';

interface Props {
event: AnalysisEvent;
@@ -36,6 +39,23 @@ export default function Event({ event }: Props) {
);
}

if (isRichQualityGateEvent(event)) {
return (
<div className="overview-analysis-event">
<span className="note">{translate('event.category', event.category)}:</span>{' '}
{event.qualityGate.stillFailing ? (
<FormattedMessage
defaultMessage={translate('event.quality_gate.still_x')}
id="event.quality_gate.still_x"
values={{ status: <Level level={event.qualityGate.status} small={true} /> }}
/>
) : (
<Level level={event.qualityGate.status} small={true} />
)}
</div>
);
}

return (
<div className="overview-analysis-event">
<span className="note">{translate('event.category', event.category)}:</span>{' '}

+ 15
- 0
server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.tsx View File

@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import Event from '../Event';
import { RichQualityGateEvent } from '../../../projectActivity/components/RichQualityGateEventInner';

const EVENT = { key: '1', category: 'OTHER', name: 'test' };
const VERSION = { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' };
@@ -31,3 +32,17 @@ it('should render an event correctly', () => {
it('should render a version correctly', () => {
expect(shallow(<Event event={VERSION} />)).toMatchSnapshot();
});

it('should render rich quality gate event', () => {
const event: RichQualityGateEvent = {
category: 'QUALITY_GATE',
key: 'foo1234',
name: '',
qualityGate: {
failing: [{ branch: 'master', key: 'foo', name: 'Foo' }],
status: 'ERROR',
stillFailing: true
}
};
expect(shallow(<Event event={event} />)).toMatchSnapshot();
});

+ 26
- 0
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.tsx.snap View File

@@ -25,3 +25,29 @@ exports[`should render an event correctly 1`] = `
</strong>
</div>
`;

exports[`should render rich quality gate event 1`] = `
<div
className="overview-analysis-event"
>
<span
className="note"
>
event.category.QUALITY_GATE
:
</span>
<FormattedMessage
defaultMessage="event.quality_gate.still_x"
id="event.quality_gate.still_x"
values={
Object {
"status": <Level
level="ERROR"
small={true}
/>,
}
}
/>
</div>
`;

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx View File

@@ -86,7 +86,7 @@ export class Meta extends React.PureComponent<Props> {
}

return (
<div className="overview-meta-card">
<div className="overview-meta-card" id="overview-meta-quality-gate">
{qualityGate && (
<MetaQualityGate
organization={organizationsEnabled ? component.organization : undefined}

+ 25
- 12
server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx View File

@@ -18,6 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as classNames from 'classnames';
import { isRichQualityGateEvent, RichQualityGateEventInner } from './RichQualityGateEventInner';
import { AnalysisEvent } from '../../../app/types';
import ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon';
import { translate } from '../../../helpers/l10n';
@@ -27,17 +29,28 @@ interface Props {
}

export default function EventInner({ event }: Props) {
return (
<div className="project-activity-event-inner">
<div className="project-activity-event-inner-icon little-spacer-right">
<ProjectEventIcon
className={'project-activity-event-icon margin-align ' + event.category}
/>
if (isRichQualityGateEvent(event)) {
return <RichQualityGateEventInner event={event} />;
} else {
return (
<div className="project-activity-event-inner">
<div className="project-activity-event-inner-main">
<ProjectEventIcon
className={classNames(
'project-activity-event-icon',
'little-spacer-right',
event.category
)}
/>

<span className="project-activity-event-inner-text">
<span className="note little-spacer-right">
{translate('event.category', event.category)}:
</span>
<strong title={event.description}>{event.name}</strong>
</span>
</div>
</div>
<span className="project-activity-event-inner-text">
<span className="note">{translate('event.category', event.category)}:</span>{' '}
<strong title={event.description}>{event.name}</strong>
</span>
</div>
);
);
}
}

+ 1
- 5
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx View File

@@ -108,7 +108,6 @@ export default class ProjectActivityAnalysis extends React.PureComponent<Props,
render() {
const { analysis, isFirst, canAdmin } = this.props;
const { date, events } = analysis;
const analysisTitle = translate('project_activity.analysis');
const hasVersion = events.find(event => event.category === 'VERSION') != null;

const canAddVersion = canAdmin && !hasVersion && this.props.canCreateVersion;
@@ -117,16 +116,13 @@ export default class ProjectActivityAnalysis extends React.PureComponent<Props,

return (
<li
className={classNames('project-activity-analysis clearfix', {
selected: this.props.selected
})}
className={classNames('project-activity-analysis', { selected: this.props.selected })}
data-date={date.valueOf()}
onClick={this.handleClick}
tabIndex={0}>
<div className="project-activity-time spacer-right">
<TimeTooltipFormatter className="text-middle" date={date} />
</div>
<div className="project-activity-analysis-icon spacer-right" title={analysisTitle} />

{(canAddVersion || canAddEvent || canDeleteAnalyses) && (
<div className="project-activity-analysis-actions big-spacer-right">

+ 117
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx View File

@@ -0,0 +1,117 @@
/*
* 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 { Link } from 'react-router';
import * as classNames from 'classnames';
import { AnalysisEvent } from '../../../app/types';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
import ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon';
import { ResetButtonLink } from '../../../components/ui/buttons';
import Level from '../../../components/ui/Level';
import { translate } from '../../../helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls';

export type RichQualityGateEvent = Exclude<AnalysisEvent, 'qualityGate'> &
Required<Pick<AnalysisEvent, 'qualityGate'>>;

export function isRichQualityGateEvent(event: AnalysisEvent): event is RichQualityGateEvent {
return event.category === 'QUALITY_GATE' && event.qualityGate !== undefined;
}

interface Props {
event: RichQualityGateEvent;
}

interface State {
expanded: boolean;
}

export class RichQualityGateEventInner extends React.PureComponent<Props, State> {
state: State = { expanded: false };

toggleProjectsList = () => {
this.setState(state => ({ expanded: !state.expanded }));
};

render() {
const { event } = this.props;
const { expanded } = this.state;
return (
<div className="project-activity-event-inner">
<div className="project-activity-event-inner-main">
<ProjectEventIcon
className={classNames(
'project-activity-event-icon',
'little-spacer-right',
event.category
)}
/>

<div className="project-activity-event-inner-text flex-1">
<span className="note little-spacer-right">
{translate('event.category', event.category)}:
</span>
{event.qualityGate.stillFailing ? (
<FormattedMessage
defaultMessage={translate('event.quality_gate.still_x')}
id="event.quality_gate.still_x"
values={{ status: <Level level={event.qualityGate.status} small={true} /> }}
/>
) : (
<Level level={event.qualityGate.status} small={true} />
)}
</div>

{event.qualityGate.failing.length > 0 && (
<ResetButtonLink
className="project-activity-event-inner-more-link"
onClick={this.toggleProjectsList}
stopPropagation={true}>
{expanded ? translate('hide') : translate('more')}
<DropdownIcon className="little-spacer-left" turned={expanded} />
</ResetButtonLink>
)}
</div>

{expanded && (
<ul>
{event.qualityGate.failing.map(project => (
<li className="display-flex-center little-spacer-top" key={project.key}>
<Level
className="little-spacer-right"
level={event.qualityGate.status}
small={true}
/>
<div className="flex-1 text-ellipsis">
<Link
onClick={e => e.stopPropagation()}
to={getProjectUrl(project.key, project.branch)}>
{project.name}
</Link>
</div>
</li>
))}
</ul>
)}
</div>
);
}
}

+ 55
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/RichQualityGateEventInner-test.tsx View File

@@ -0,0 +1,55 @@
/*
* 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 { RichQualityGateEventInner, RichQualityGateEvent } from '../RichQualityGateEventInner';
import { click } from '../../../../helpers/testUtils';

const event: RichQualityGateEvent = {
category: 'QUALITY_GATE',
key: 'foo1234',
name: '',
qualityGate: {
failing: [
{ branch: 'master', key: 'foo', name: 'Foo' },
{ branch: 'master', key: 'bar', name: 'Bar' }
],
status: 'ERROR',
stillFailing: true
}
};

it('should render', () => {
const wrapper = shallow(<RichQualityGateEventInner event={event} />);
expect(wrapper).toMatchSnapshot();

click(wrapper.find('.project-activity-event-inner-more-link'));
wrapper.update();
expect(wrapper).toMatchSnapshot();
});

it('should not expand', () => {
const wrapper = shallow(
<RichQualityGateEventInner
event={{ ...event, qualityGate: { ...event.qualityGate, failing: [] } }}
/>
);
expect(wrapper.find('.project-activity-event-inner-more-link').exists()).toBe(false);
});

+ 157
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap View File

@@ -0,0 +1,157 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render 1`] = `
<div
className="project-activity-event-inner"
>
<div
className="project-activity-event-inner-main"
>
<ProjectEventIcon
className="project-activity-event-icon little-spacer-right QUALITY_GATE"
/>
<div
className="project-activity-event-inner-text flex-1"
>
<span
className="note little-spacer-right"
>
event.category.QUALITY_GATE
:
</span>
<FormattedMessage
defaultMessage="event.quality_gate.still_x"
id="event.quality_gate.still_x"
values={
Object {
"status": <Level
level="ERROR"
small={true}
/>,
}
}
/>
</div>
<ResetButtonLink
className="project-activity-event-inner-more-link"
onClick={[Function]}
stopPropagation={true}
>
more
<DropdownIcon
className="little-spacer-left"
turned={false}
/>
</ResetButtonLink>
</div>
</div>
`;

exports[`should render 2`] = `
<div
className="project-activity-event-inner"
>
<div
className="project-activity-event-inner-main"
>
<ProjectEventIcon
className="project-activity-event-icon little-spacer-right QUALITY_GATE"
/>
<div
className="project-activity-event-inner-text flex-1"
>
<span
className="note little-spacer-right"
>
event.category.QUALITY_GATE
:
</span>
<FormattedMessage
defaultMessage="event.quality_gate.still_x"
id="event.quality_gate.still_x"
values={
Object {
"status": <Level
level="ERROR"
small={true}
/>,
}
}
/>
</div>
<ResetButtonLink
className="project-activity-event-inner-more-link"
onClick={[Function]}
stopPropagation={true}
>
hide
<DropdownIcon
className="little-spacer-left"
turned={true}
/>
</ResetButtonLink>
</div>
<ul>
<li
className="display-flex-center little-spacer-top"
key="foo"
>
<Level
className="little-spacer-right"
level="ERROR"
small={true}
/>
<div
className="flex-1 text-ellipsis"
>
<Link
onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": "master",
"id": "foo",
},
}
}
>
Foo
</Link>
</div>
</li>
<li
className="display-flex-center little-spacer-top"
key="bar"
>
<Level
className="little-spacer-right"
level="ERROR"
small={true}
/>
<div
className="flex-1 text-ellipsis"
>
<Link
onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": "master",
"id": "bar",
},
}
}
>
Bar
</Link>
</div>
</li>
</ul>
</div>
`;

+ 101
- 101
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css View File

@@ -61,78 +61,6 @@
flex-shrink: 0;
}

.project-activity-graphs {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
}

.project-activity-graph-container {
padding: 10px 0;
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
}

.project-activity-graph {
flex: 1;
overflow: hidden;
}

.project-activity-graph-legends {
flex-grow: 0;
padding-bottom: 16px;
text-align: center;
}

.project-activity-graph-legend-actionable {
display: inline-block;
padding: 4px 8px 4px 12px;
border-width: 1px;
border-style: solid;
border-radius: 12px;
}

.project-activity-graph-tooltip {
padding: 8px;
}

.project-activity-graph-tooltip-line {
height: 20px;
}

.project-activity-graph-tooltip-line + .project-activity-graph-tooltip-line {
padding-top: 4px;
}

.project-activity-graph-tooltip-line .project-activity-event-icon {
margin-top: 1px;
}

.project-activity-graph-tooltip-issues-line {
height: 26px;
padding-bottom: 4px;
}

.project-activity-graph-tooltip-separator {
padding-left: 16px;
padding-right: 16px;
}

.project-activity-graph-tooltip-separator hr {
margin-top: 8px;
margin-bottom: 8px;
}

.project-activity-graph-tooltip-title,
.project-activity-graph-tooltip-value {
font-weight: bold;
}

.project-activity-days-list {
}

@@ -156,8 +84,9 @@

.project-activity-analysis {
position: relative;
min-height: 20px;
padding: 4px;
display: flex;
min-height: var(--smallControlHeight);
padding: calc(0.5 * var(--gridSize));
border-top: 1px solid var(--barBorderColor);
border-bottom: 1px solid var(--barBorderColor);
cursor: pointer;
@@ -180,36 +109,30 @@
}

.project-activity-analysis-actions {
float: left;
flex-shrink: 0;
flex-grow: 0;
height: var(--smallControlHeight);
}

.project-activity-time {
float: left;
flex-shrink: 0;
flex-grow: 0;
width: 54px;
line-height: 20px;
height: var(--smallControlHeight);
line-height: var(--smallControlHeight);
box-sizing: border-box;
font-size: var(--smallFontSize);
font-weight: bold;
text-align: right;
}

.project-activity-analysis-icon {
float: left;
width: 10px;
height: 10px;
margin-top: 5px;
border: 2px solid var(--blue);
border-radius: 10px;
box-sizing: border-box;
content: '';
}

.project-activity-events {
overflow: hidden;
flex: 1;
min-width: 0;
}

.project-activity-event {
line-height: 18px;
line-height: var(--smallControlHeight);
display: flex;
}

@@ -219,28 +142,32 @@

.project-activity-event-inner {
flex: 1;
min-width: 0;
}

.project-activity-event-inner-main {
display: flex;
flex-direction: row;
overflow: hidden;
align-items: flex-start;
}

.project-activity-event-inner-icon {
.project-activity-event-icon {
flex-shrink: 0;
flex-grow: 0;
margin-top: calc(0.5 * var(--smallControlHeight) - 7px);
}

.project-activity-event-inner-text {
flex: 1;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
line-height: var(--smallControlHeight);
}

.project-activity-event-actions {
display: inline-block;
.project-activity-event-inner-more-link {
line-height: 16px;
margin-top: 2px;
}

.project-activity-event-inner-icon .project-activity-event-icon {
margin-top: 3px;
.project-activity-event-actions {
flex-shrink: 0;
flex-grow: 0;
}

.project-activity-event-icon.VERSION {
@@ -289,3 +216,76 @@
overflow: hidden;
text-overflow: ellipsis;
}

.project-activity-graphs {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
}

.project-activity-graph-container {
padding: 10px 0;
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
}

.project-activity-graph {
flex: 1;
overflow: hidden;
}

.project-activity-graph-legends {
flex-grow: 0;
padding-bottom: 16px;
text-align: center;
}

.project-activity-graph-legend-actionable {
display: inline-block;
padding: 4px 8px 4px 12px;
border-width: 1px;
border-style: solid;
border-radius: 12px;
}

.project-activity-graph-tooltip {
padding: 8px;
}

.project-activity-graph-tooltip-line {
height: 20px;
}

.project-activity-graph-tooltip-line + .project-activity-graph-tooltip-line {
padding-top: 4px;
}

.Select .project-activity-event-icon,
.project-activity-graph-tooltip-line .project-activity-event-icon {
margin-top: 1px;
}

.project-activity-graph-tooltip-issues-line {
height: 26px;
padding-bottom: 4px;
}

.project-activity-graph-tooltip-separator {
padding-left: 16px;
padding-right: 16px;
}

.project-activity-graph-tooltip-separator hr {
margin-top: 8px;
margin-bottom: 8px;
}

.project-activity-graph-tooltip-title,
.project-activity-graph-tooltip-value {
font-weight: bold;
}

+ 2
- 2
server/sonar-web/src/main/js/components/ui/Level.css View File

@@ -42,8 +42,8 @@
padding-right: 9px;
margin-top: -1px;
margin-bottom: -1px;
height: 18px;
line-height: 18px;
height: var(--smallControlHeight);
line-height: var(--smallControlHeight);
font-size: var(--smallFontSize);
}


+ 1
- 1
server/sonar-web/src/main/js/components/ui/Level.tsx View File

@@ -30,7 +30,7 @@ interface Props {
}

export default function Level(props: Props) {
const formatted = formatMeasure(props.level, 'LEVEL', null);
const formatted = formatMeasure(props.level, 'LEVEL');
const className = classNames(props.className, 'level', 'level-' + props.level, {
'level-small': props.small,
'level-muted': props.muted

+ 1
- 1
server/sonar-web/src/main/js/components/ui/buttons.css View File

@@ -119,6 +119,7 @@
.button-link {
display: inline;
height: auto; /* Keep this to not inherit the height from .button */
line-height: 1;
margin: 0;
padding: 0;
border: none;
@@ -128,7 +129,6 @@
border-bottom: 1px solid var(--lightBlue);
font-weight: 400;
font-size: inherit;
line-height: inherit;
transition: all 0.2s ease;
}


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

@@ -437,6 +437,7 @@ event.category.VERSION=Version
event.category.QUALITY_GATE=Quality Gate
event.category.QUALITY_PROFILE=Quality Profile
event.category.OTHER=Other
event.quality_gate.still_x=Still {status}


#------------------------------------------------------------------------------

Loading…
Cancel
Save