Quellcode durchsuchen

SONAR-12637 Improve activity list

tags/8.2.0.32929
Wouter Admiraal vor 4 Jahren
Ursprung
Commit
9a63cf1972
27 geänderte Dateien mit 1492 neuen und 1158 gelöschten Zeilen
  1. 5
    0
      server/sonar-web/src/main/js/app/styles/init/misc.css
  2. 6
    20
      server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx
  3. 78
    97
      server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx
  4. 11
    23
      server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx
  5. 16
    12
      server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx
  6. 11
    37
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx
  7. 119
    150
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx
  8. 0
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
  9. 15
    33
      server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx
  10. 106
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Event-test.tsx
  11. 75
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/EventInner-test.tsx
  12. 43
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Events-test.tsx
  13. 78
    46
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx
  14. 77
    25
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx
  15. 30
    60
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/DefinitionChangeEventInner-test.tsx.snap
  16. 91
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Event-test.tsx.snap
  17. 101
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/EventInner-test.tsx.snap
  18. 98
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Events-test.tsx.snap
  19. 261
    7
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap
  20. 126
    493
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap
  21. 0
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap
  22. 45
    67
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap
  23. 15
    20
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.tsx
  24. 56
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/RemoveEventForm-test.tsx
  25. 13
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/RemoveEventForm-test.tsx.snap
  26. 15
    63
      server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
  27. 1
    3
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx

+ 5
- 0
server/sonar-web/src/main/js/app/styles/init/misc.css Datei anzeigen

@@ -344,6 +344,11 @@ th.huge-spacer-right {
align-items: stretch;
}

.display-flex-start {
display: flex !important;
align-items: flex-start;
}

.display-inline-flex-baseline {
display: inline-flex !important;
align-items: baseline;

+ 6
- 20
server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx Datei anzeigen

@@ -17,14 +17,12 @@
* 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 classNames from 'classnames';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import BranchIcon from 'sonar-ui-common/components/icons/BranchIcon';
import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
import ProjectEventIcon from 'sonar-ui-common/components/icons/ProjectEventIcon';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { limitComponentName } from 'sonar-ui-common/helpers/path';
import { isMainBranch } from '../../../helpers/branch-like';
@@ -136,22 +134,10 @@ export class DefinitionChangeEventInner extends React.PureComponent<Props, State
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>
</div>
<>
<span className="note">{translate('event.category', event.category)}:</span>

<div>
<ResetButtonLink
className="project-activity-event-inner-more-link"
onClick={this.toggleProjectsList}
@@ -162,15 +148,15 @@ export class DefinitionChangeEventInner extends React.PureComponent<Props, State
</div>

{expanded && (
<ul className="project-activity-event-inner-more-content">
<ul className="spacer-left spacer-top">
{event.definitionChange.projects.map(project => (
<li className="display-flex-center little-spacer-top" key={project.key}>
<li className="display-flex-center spacer-top" key={project.key}>
{this.renderProjectChange(project)}
</li>
))}
</ul>
)}
</div>
</>
);
}
}

+ 78
- 97
server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx Datei anzeigen

@@ -17,120 +17,101 @@
* 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 classNames from 'classnames';
import * as React from 'react';
import { DeleteButton, EditButton } from 'sonar-ui-common/components/controls/buttons';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import ProjectEventIcon from 'sonar-ui-common/components/icons/ProjectEventIcon';
import { translate } from 'sonar-ui-common/helpers/l10n';
import EventInner from './EventInner';
import ChangeEventForm from './forms/ChangeEventForm';
import RemoveEventForm from './forms/RemoveEventForm';

interface Props {
analysis: string;
export interface EventProps {
analysisKey: string;
canAdmin?: boolean;
changeEvent: (event: string, name: string) => Promise<void>;
deleteEvent: (analysis: string, event: string) => Promise<void>;
event: T.AnalysisEvent;
isFirst?: boolean;
onChange?: (event: string, name: string) => Promise<void>;
onDelete?: (analysisKey: string, event: string) => Promise<void>;
}

interface State {
changing: boolean;
deleting: boolean;
}

export default class Event extends React.PureComponent<Props, State> {
mounted = false;
state: State = { changing: false, deleting: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

startChanging = () => {
this.setState({ changing: true });
};

stopChanging = () => {
if (this.mounted) {
this.setState({ changing: false });
}
};
export function Event(props: EventProps) {
const { analysisKey, event, canAdmin, isFirst } = props;

startDeleting = () => {
this.setState({ deleting: true });
};
const [changing, setChanging] = React.useState(false);
const [deleting, setDeleting] = React.useState(false);

stopDeleting = () => {
if (this.mounted) {
this.setState({ deleting: false });
}
};
const isOther = event.category === 'OTHER';
const isVersion = event.category === 'VERSION';
const canChange = (isOther || isVersion) && props.onChange;
const canDelete = (isOther || (isVersion && !isFirst)) && props.onDelete;
const showActions = canAdmin && (canChange || canDelete);

render() {
const { event, canAdmin } = this.props;
const isOther = event.category === 'OTHER';
const isVersion = !isOther && event.category === 'VERSION';
const canChange = isOther || isVersion;
const canDelete = isOther || (isVersion && !this.props.isFirst);
const showActions = canAdmin && (canChange || canDelete);
return (
<div className="project-activity-event">
<ProjectEventIcon
className={classNames(
'project-activity-event-icon little-spacer-right text-middle',
event.category
)}
/>

return (
<div className="project-activity-event">
<EventInner event={this.props.event} />
<EventInner event={event} />

{showActions && (
<div className="project-activity-event-actions spacer-left">
{canChange && (
<Tooltip overlay={translate('project_activity.events.tooltip.edit')}>
<EditButton className="js-change-event button-small" onClick={this.startChanging} />
</Tooltip>
)}
{canDelete && (
<Tooltip overlay={translate('project_activity.events.tooltip.delete')}>
<DeleteButton
className="js-delete-event button-small"
onClick={this.startDeleting}
/>
</Tooltip>
)}
</div>
)}
{showActions && (
<span className="nowrap">
{canChange && (
<EditButton
aria-label={translate('project_activity.events.tooltip.edit')}
className="button-small"
data-test="project-activity__edit-event"
onClick={() => setChanging(true)}
stopPropagation={true}
/>
)}
{canDelete && (
<DeleteButton
aria-label={translate('project_activity.events.tooltip.delete')}
className="button-small"
data-test="project-activity__delete-event"
onClick={() => setDeleting(true)}
stopPropagation={true}
/>
)}
</span>
)}

{this.state.changing && (
<ChangeEventForm
changeEvent={this.props.changeEvent}
event={this.props.event}
header={
isVersion
? translate('project_activity.change_version')
: translate('project_activity.change_custom_event')
}
onClose={this.stopChanging}
/>
)}
{changing && props.onChange && (
<ChangeEventForm
changeEvent={props.onChange}
event={event}
header={
isVersion
? translate('project_activity.change_version')
: translate('project_activity.change_custom_event')
}
onClose={() => setChanging(false)}
/>
)}

{this.state.deleting && (
<RemoveEventForm
analysis={this.props.analysis}
deleteEvent={this.props.deleteEvent}
event={this.props.event}
header={
isVersion
? translate('project_activity.remove_version')
: translate('project_activity.remove_custom_event')
}
onClose={this.stopDeleting}
removeEventQuestion={`project_activity.${
isVersion ? 'remove_version' : 'remove_custom_event'
}.question`}
/>
)}
</div>
);
}
{deleting && props.onDelete && (
<RemoveEventForm
analysisKey={analysisKey}
event={event}
header={
isVersion
? translate('project_activity.remove_version')
: translate('project_activity.remove_custom_event')
}
onClose={() => setDeleting(false)}
onConfirm={props.onDelete}
removeEventQuestion={translate(
`project_activity.${isVersion ? 'remove_version' : 'remove_custom_event'}.question`
)}
/>
)}
</div>
);
}

export default React.memo(Event);

+ 11
- 23
server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx Datei anzeigen

@@ -17,19 +17,18 @@
* 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 classNames from 'classnames';
import * as React from 'react';
import ProjectEventIcon from 'sonar-ui-common/components/icons/ProjectEventIcon';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { ComponentContext } from '../../../app/components/ComponentContext';
import { DefinitionChangeEventInner, isDefinitionChangeEvent } from './DefinitionChangeEventInner';
import { isRichQualityGateEvent, RichQualityGateEventInner } from './RichQualityGateEventInner';

interface Props {
export interface EventInnerProps {
event: T.AnalysisEvent;
}

export default function EventInner({ event }: Props) {
export default function EventInner({ event }: EventInnerProps) {
if (isRichQualityGateEvent(event)) {
return <RichQualityGateEventInner event={event} />;
} else if (isDefinitionChangeEvent(event)) {
@@ -39,25 +38,14 @@ export default function EventInner({ event }: Props) {
</ComponentContext.Consumer>
);
} 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>
const content = (
<span className="text-middle">
<span className="note little-spacer-right">
{translate('event.category', event.category)}:
</span>
<strong className="spacer-right">{event.name}</strong>
</span>
);
return event.description ? <Tooltip overlay={event.description}>{content}</Tooltip> : content;
}
}

+ 16
- 12
server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx Datei anzeigen

@@ -21,18 +21,20 @@ import { sortBy } from 'lodash';
import * as React from 'react';
import Event from './Event';

interface Props {
analysis: string;
export interface EventsProps {
analysisKey: string;
canAdmin?: boolean;
changeEvent: (event: string, name: string) => Promise<void>;
deleteEvent: (analysis: string, event: string) => Promise<void>;
events: T.AnalysisEvent[];
isFirst?: boolean;
onChange?: (event: string, name: string) => Promise<void>;
onDelete?: (analysis: string, event: string) => Promise<void>;
}

export default function Events(props: Props) {
export function Events(props: EventsProps) {
const { analysisKey, canAdmin, events, isFirst } = props;

const sortedEvents = sortBy(
props.events,
events,
// versions last
event => (event.category === 'VERSION' ? 1 : 0),
// then the rest sorted by category
@@ -40,18 +42,20 @@ export default function Events(props: Props) {
);

return (
<div className="project-activity-events">
<div className="big-spacer-top">
{sortedEvents.map(event => (
<Event
analysis={props.analysis}
canAdmin={props.canAdmin}
changeEvent={props.changeEvent}
deleteEvent={props.deleteEvent}
analysisKey={analysisKey}
canAdmin={canAdmin}
event={event}
isFirst={props.isFirst}
isFirst={isFirst}
key={event.key}
onChange={props.onChange}
onDelete={props.onDelete}
/>
))}
</div>
);
}

export default React.memo(Events);

+ 11
- 37
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx Datei anzeigen

@@ -25,12 +25,8 @@ import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { toShortNotSoISOString } from 'sonar-ui-common/helpers/dates';
import { translate } from 'sonar-ui-common/helpers/l10n';
import DateFormatter from '../../../components/intl/DateFormatter';
import {
activityQueryChanged,
getAnalysesByVersionByDay,
Query,
selectedDateQueryChanged
} from '../utils';
import { ComponentQualifier } from '../../../types/component';
import { activityQueryChanged, getAnalysesByVersionByDay, Query } from '../utils';
import ProjectActivityAnalysis from './ProjectActivityAnalysis';

interface Props {
@@ -41,7 +37,6 @@ interface Props {
canAdmin?: boolean;
canDeleteAnalyses?: boolean;
changeEvent: (event: string, name: string) => Promise<void>;
className?: string;
deleteAnalysis: (analysis: string) => Promise<void>;
deleteEvent: (analysis: string, event: string) => Promise<void>;
initializing: boolean;
@@ -54,7 +49,7 @@ interface Props {
export default class ProjectActivityAnalysesList extends React.PureComponent<Props> {
analyses?: HTMLCollectionOf<HTMLElement>;
badges?: HTMLCollectionOf<HTMLElement>;
scrollContainer?: HTMLElement | null;
scrollContainer?: HTMLUListElement | null;

constructor(props: Props) {
super(props);
@@ -74,13 +69,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro
if (!this.scrollContainer) {
return;
}
if (
this.props.query.selectedDate &&
(selectedDateQueryChanged(prevProps.query, this.props.query) ||
prevProps.analyses !== this.props.analyses)
) {
this.scrollToDate(this.props.query.selectedDate);
} else if (activityQueryChanged(prevProps.query, this.props.query)) {
if (activityQueryChanged(prevProps.query, this.props.query)) {
this.resetScrollTop(0, true);
}
}
@@ -100,24 +89,6 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro
this.updateStickyBadges(forceBadgeAlignement);
};

scrollToDate = (targetDate?: Date) => {
if (!this.scrollContainer || !targetDate || !this.analyses) {
return;
}
const date = targetDate.valueOf();
for (let i = 1; i < this.analyses.length; i++) {
if (Number(this.analyses[i].getAttribute('data-date')) === date) {
const containerHeight = this.scrollContainer.offsetHeight - 100;
const scrollDiff = Math.abs(this.scrollContainer.scrollTop - this.analyses[i].offsetTop);
// Center only the extremities and the ones outside of the container
if (scrollDiff > containerHeight || scrollDiff < 100) {
this.resetScrollTop(this.analyses[i].offsetTop - containerHeight / 2);
}
break;
}
}
};

updateStickyBadges = (forceBadgeAlignement?: boolean) => {
if (!this.scrollContainer || !this.badges) {
return;
@@ -173,7 +144,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro
addVersion={this.props.addVersion}
analysis={analysis}
canAdmin={this.props.canAdmin}
canCreateVersion={this.props.project.qualifier === 'TRK'}
canCreateVersion={this.props.project.qualifier === ComponentQualifier.Project}
canDeleteAnalyses={this.props.canDeleteAnalyses}
changeEvent={this.props.changeEvent}
deleteAnalysis={this.props.deleteAnalysis}
@@ -181,6 +152,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro
isBaseline={this.shouldRenderBaselineMarker(analysis)}
isFirst={analysis.key === firstAnalysisKey}
key={analysis.key}
parentScrollContainer={this.scrollContainer}
selected={analysis.date.valueOf() === selectedDate}
updateSelectedDate={this.updateSelectedDate}
/>
@@ -194,7 +166,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro
(byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
if (this.props.analyses.length === 0 || !hasFilteredData) {
return (
<div className={this.props.className}>
<div className="boxed-group-inner">
{this.props.initializing ? (
<div className="text-center">
<i className="spinner" />
@@ -208,10 +180,12 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro

return (
<ul
className={classNames('project-activity-versions-list', this.props.className)}
className="project-activity-versions-list"
onScroll={this.handleScroll}
ref={element => (this.scrollContainer = element)}
style={{ paddingTop: this.props.project.qualifier === 'TRK' ? 52 : undefined }}>
style={{
paddingTop: this.props.project.qualifier === ComponentQualifier.Project ? 52 : undefined
}}>
{byVersionByDay.map((version, idx) => {
const days = Object.keys(version.byDay);
if (days.length <= 0) {

+ 119
- 150
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx Datei anzeigen

@@ -23,16 +23,18 @@ import ActionsDropdown, {
ActionsDropdownDivider,
ActionsDropdownItem
} from 'sonar-ui-common/components/controls/ActionsDropdown';
import ClickEventBoundary from 'sonar-ui-common/components/controls/ClickEventBoundary';
import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
import { parseDate } from 'sonar-ui-common/helpers/dates';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
import TimeFormatter from '../../../components/intl/TimeFormatter';
import Events from './Events';
import AddEventForm from './forms/AddEventForm';
import RemoveAnalysisForm from './forms/RemoveAnalysisForm';

interface Props {
export interface ProjectActivityAnalysisProps {
addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
addVersion: (analysis: string, version: string) => Promise<void>;
analysis: T.ParsedAnalysis;
@@ -44,141 +46,93 @@ interface Props {
deleteEvent: (analysis: string, event: string) => Promise<void>;
isBaseline: boolean;
isFirst: boolean;
parentScrollContainer?: HTMLElement | null;
selected: boolean;
updateSelectedDate: (date: Date) => void;
}

interface State {
addEventForm: boolean;
addVersionForm: boolean;
removeAnalysisForm: boolean;
}

export default class ProjectActivityAnalysis extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
addEventForm: false,
addVersionForm: false,
removeAnalysisForm: false
};

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleClick = () => {
this.props.updateSelectedDate(this.props.analysis.date);
};

stopPropagation = (event: React.SyntheticEvent) => {
event.stopPropagation();
};

handleRemoveAnalysisClick = () => {
this.setState({ removeAnalysisForm: true });
};

closeRemoveAnalysisForm = () => {
if (this.mounted) {
this.setState({ removeAnalysisForm: false });
}
};

handleAddEventClick = () => {
this.setState({ addEventForm: true });
};

closeAddEventForm = () => {
if (this.mounted) {
this.setState({ addEventForm: false });
export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
let node: HTMLLIElement | null = null;

const {
analysis,
isBaseline,
isFirst,
canAdmin,
canCreateVersion,
parentScrollContainer,
selected
} = props;

React.useEffect(() => {
if (node && parentScrollContainer && selected) {
const { height } = node.getBoundingClientRect();
scrollToElement(node, {
bottomOffset: height + 20,
topOffset: 60,
parent: parentScrollContainer,
smooth: false
});
}
};

handleAddVersionClick = () => {
this.setState({ addVersionForm: true });
};

closeAddVersionForm = () => {
if (this.mounted) {
this.setState({ addVersionForm: false });
}
};

renderBaselineMarker() {
return (
<div className="baseline-marker">
<div className="wedge" />
<hr />
<div className="label display-flex-center">
{translate('project_activity.new_code_period_start')}
<HelpTooltip
className="little-spacer-left"
overlay={translate('project_activity.new_code_period_start.help')}
placement="top"
/>
});

const [addEventForm, setAddEventForm] = React.useState(false);
const [addVersionForm, setAddVersionForm] = React.useState(false);
const [removeAnalysisForm, setRemoveAnalysisForm] = React.useState(false);

const parsedDate = parseDate(analysis.date);
const hasVersion = analysis.events.find(event => event.category === 'VERSION') != null;

const canAddVersion = canAdmin && !hasVersion && canCreateVersion;
const canAddEvent = canAdmin;
const canDeleteAnalyses =
props.canDeleteAnalyses && !isFirst && !analysis.manualNewCodePeriodBaseline;

return (
<li
className={classNames('project-activity-analysis bordered-top bordered-bottom', {
selected
})}
onClick={() => props.updateSelectedDate(analysis.date)}
ref={ref => (node = ref)}>
<div className="display-flex-center display-flex-space-between">
<div className="project-activity-time">
<TimeFormatter date={parsedDate} long={false}>
{formattedTime => (
<time className="text-middle" dateTime={parsedDate.toISOString()}>
{formattedTime}
</time>
)}
</TimeFormatter>
</div>
</div>
);
}

render() {
const { analysis, isBaseline, isFirst, canAdmin, canCreateVersion } = this.props;
const { date, events } = analysis;
const parsedDate = parseDate(date);
const hasVersion = events.find(event => event.category === 'VERSION') != null;

const canAddVersion = canAdmin && !hasVersion && canCreateVersion;
const canAddEvent = canAdmin;
const canDeleteAnalyses =
this.props.canDeleteAnalyses && !isFirst && !analysis.manualNewCodePeriodBaseline;

let tooltipContent = <TimeFormatter date={parsedDate} long={true} />;
if (analysis.buildString) {
tooltipContent = (
<>
{tooltipContent}
<br />
{translateWithParameters(
'project_activity.analysis_build_string_X',
analysis.buildString
)}
</>
);
}

return (
<Tooltip mouseEnterDelay={0.5} overlay={tooltipContent} placement="left">
<li
className={classNames('project-activity-analysis', { selected: this.props.selected })}
data-date={parsedDate.valueOf()}
onClick={this.handleClick}
tabIndex={0}>
<div className="project-activity-time spacer-right">
<TimeFormatter date={parsedDate} long={false}>
{formattedTime => (
<time className="text-middle" dateTime={parsedDate.toISOString()}>
{formattedTime}
</time>
)}
</TimeFormatter>
{analysis.buildString && (
<div className="flex-shrink small text-muted text-ellipsis">
{translateWithParameters(
'project_activity.analysis_build_string_X',
analysis.buildString
)}
</div>

{(canAddVersion || canAddEvent || canDeleteAnalyses) && (
<div className="project-activity-analysis-actions big-spacer-right">
<ActionsDropdown small={true} toggleClassName="js-analysis-actions">
)}

{(canAddVersion || canAddEvent || canDeleteAnalyses) && (
<ClickEventBoundary>
<div className="project-activity-analysis-actions big-spacer-left">
<ActionsDropdown
overlayPlacement={PopupPlacement.BottomRight}
small={true}
toggleClassName="js-analysis-actions">
{canAddVersion && (
<ActionsDropdownItem
className="js-add-event"
onClick={this.handleAddVersionClick}>
className="js-add-version"
onClick={() => setAddVersionForm(true)}>
{translate('project_activity.add_version')}
</ActionsDropdownItem>
)}
{canAddEvent && (
<ActionsDropdownItem className="js-add-event" onClick={this.handleAddEventClick}>
<ActionsDropdownItem
className="js-add-event"
onClick={() => setAddEventForm(true)}>
{translate('project_activity.add_custom_event')}
</ActionsDropdownItem>
)}
@@ -187,54 +141,69 @@ export default class ProjectActivityAnalysis extends React.PureComponent<Props,
<ActionsDropdownItem
className="js-delete-analysis"
destructive={true}
onClick={this.handleRemoveAnalysisClick}>
onClick={() => setRemoveAnalysisForm(true)}>
{translate('project_activity.delete_analysis')}
</ActionsDropdownItem>
)}
</ActionsDropdown>

{this.state.addVersionForm && (
{addVersionForm && (
<AddEventForm
addEvent={this.props.addVersion}
addEvent={props.addVersion}
addEventButtonText="project_activity.add_version"
analysis={analysis}
onClose={this.closeAddVersionForm}
onClose={() => setAddVersionForm(false)}
/>
)}

{this.state.addEventForm && (
{addEventForm && (
<AddEventForm
addEvent={this.props.addCustomEvent}
addEvent={props.addCustomEvent}
addEventButtonText="project_activity.add_custom_event"
analysis={analysis}
onClose={this.closeAddEventForm}
onClose={() => setAddEventForm(false)}
/>
)}

{this.state.removeAnalysisForm && (
{removeAnalysisForm && (
<RemoveAnalysisForm
analysis={analysis}
deleteAnalysis={this.props.deleteAnalysis}
onClose={this.closeRemoveAnalysisForm}
deleteAnalysis={props.deleteAnalysis}
onClose={() => setRemoveAnalysisForm(false)}
/>
)}
</div>
)}

{events.length > 0 && (
<Events
analysis={analysis.key}
canAdmin={canAdmin}
changeEvent={this.props.changeEvent}
deleteEvent={this.props.deleteEvent}
events={events}
isFirst={this.props.isFirst}
/>
)}
</ClickEventBoundary>
)}
</div>

{isBaseline && this.renderBaselineMarker()}
</li>
</Tooltip>
);
}
{analysis.events.length > 0 && (
<Events
analysisKey={analysis.key}
canAdmin={canAdmin}
events={analysis.events}
isFirst={isFirst}
onChange={props.changeEvent}
onDelete={props.deleteEvent}
/>
)}

{isBaseline && (
<div className="baseline-marker">
<div className="wedge" />
<hr />
<div className="label display-flex-center">
{translate('project_activity.new_code_period_start')}
<HelpTooltip
className="little-spacer-left"
overlay={translate('project_activity.new_code_period_start.help')}
placement="top"
/>
</div>
</div>
)}
</li>
);
}

export default React.memo(ProjectActivityAnalysis);

+ 0
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx Datei anzeigen

@@ -78,7 +78,6 @@ export default function ProjectActivityApp(props: Props) {
canAdmin={canAdmin}
canDeleteAnalyses={canDeleteAnalyses}
changeEvent={props.changeEvent}
className="boxed-group-inner"
deleteAnalysis={props.deleteAnalysis}
deleteEvent={props.deleteEvent}
initializing={props.initializing}

+ 15
- 33
server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx Datei anzeigen

@@ -17,13 +17,11 @@
* 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 classNames from 'classnames';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
import ProjectEventIcon from 'sonar-ui-common/components/icons/ProjectEventIcon';
import Level from 'sonar-ui-common/components/ui/Level';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls';
@@ -57,31 +55,19 @@ export class RichQualityGateEventInner extends React.PureComponent<Props, State>
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
)}
<>
<span className="note 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 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>

<div>
{event.qualityGate.failing.length > 0 && (
<ResetButtonLink
className="project-activity-event-inner-more-link"
@@ -94,14 +80,10 @@ export class RichQualityGateEventInner extends React.PureComponent<Props, State>
</div>

{expanded && (
<ul className="project-activity-event-inner-more-content">
<ul className="spacer-left spacer-top">
{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}
/>
<li className="display-flex-center spacer-top" key={project.key}>
<Level className="spacer-right" level={event.qualityGate.status} small={true} />
<div className="flex-1 text-ellipsis">
<Link
onClick={this.stopPropagation}
@@ -114,7 +96,7 @@ export class RichQualityGateEventInner extends React.PureComponent<Props, State>
))}
</ul>
)}
</div>
</>
);
}
}

+ 106
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Event-test.tsx Datei anzeigen

@@ -0,0 +1,106 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
import * as React from 'react';
import { DeleteButton, EditButton } from 'sonar-ui-common/components/controls/buttons';
import { click } from 'sonar-ui-common/helpers/testUtils';
import { mockAnalysisEvent } from '../../../../helpers/testMocks';
import { Event, EventProps } from '../Event';
import ChangeEventForm from '../forms/ChangeEventForm';
import RemoveEventForm from '../forms/RemoveEventForm';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ canAdmin: true })).toMatchSnapshot('with admin options');
});

it('should correctly allow deletion', () => {
expect(
shallowRender({
canAdmin: true,
event: mockAnalysisEvent({ category: 'VERSION' }),
isFirst: true
})
.find(DeleteButton)
.exists()
).toBe(false);

expect(
shallowRender({ canAdmin: true, event: mockAnalysisEvent() })
.find(DeleteButton)
.exists()
).toBe(false);

expect(
shallowRender({ canAdmin: true })
.find(DeleteButton)
.exists()
).toBe(true);
});

it('should correctly allow edition', () => {
expect(
shallowRender({ canAdmin: true })
.find(EditButton)
.exists()
).toBe(true);

expect(
shallowRender({ canAdmin: true, isFirst: true })
.find(EditButton)
.exists()
).toBe(true);

expect(
shallowRender({ canAdmin: true, event: mockAnalysisEvent() })
.find(EditButton)
.exists()
).toBe(false);
});

it('should correctly show edit form', () => {
const wrapper = shallowRender({ canAdmin: true });
click(wrapper.find(EditButton));
const changeEventForm = wrapper.find(ChangeEventForm);
expect(changeEventForm.exists()).toBe(true);
changeEventForm.prop('onClose')();
expect(wrapper.find(ChangeEventForm).exists()).toBe(false);
});

it('should correctly show delete form', () => {
const wrapper = shallowRender({ canAdmin: true });
click(wrapper.find(DeleteButton));
const removeEventForm = wrapper.find(RemoveEventForm);
expect(removeEventForm.exists()).toBe(true);
removeEventForm.prop('onClose')();
expect(wrapper.find(RemoveEventForm).exists()).toBe(false);
});

function shallowRender(props: Partial<EventProps> = {}) {
return shallow<Event>(
<Event
analysisKey="foo"
event={mockAnalysisEvent({ category: 'OTHER' })}
onChange={jest.fn()}
onDelete={jest.fn()}
{...props}
/>
);
}

+ 75
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/EventInner-test.tsx Datei anzeigen

@@ -0,0 +1,75 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
import * as React from 'react';
import { mockAnalysisEvent } from '../../../../helpers/testMocks';
import { BranchLike } from '../../../../types/branch-like';
import EventInner, { EventInnerProps } from '../EventInner';

jest.mock('../../../../app/components/ComponentContext', () => {
const { mockBranch } = jest.requireActual('../../../../helpers/mocks/branch-like');
return {
ComponentContext: {
Consumer: ({
children
}: {
children: (props: { branchLike: BranchLike }) => React.ReactNode;
}) => {
return children({ branchLike: mockBranch() });
}
}
};
});

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(
shallowRender({
event: mockAnalysisEvent({
category: 'VERSION',
description: undefined,
qualityGate: undefined
})
})
).toMatchSnapshot('no description');
expect(shallowRender({ event: mockAnalysisEvent() })).toMatchSnapshot('rich quality gate');
expect(
shallowRender({
event: mockAnalysisEvent({
category: 'DEFINITION_CHANGE',
definitionChange: {
projects: [{ changeType: 'ADDED', key: 'foo', name: 'Foo' }]
},
qualityGate: undefined
})
})
.find('Consumer')
.dive()
).toMatchSnapshot('definition change');
});

function shallowRender(props: Partial<EventInnerProps> = {}) {
return shallow(
<EventInner
event={mockAnalysisEvent({ category: 'VERSION', qualityGate: undefined })}
{...props}
/>
);
}

+ 43
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Events-test.tsx Datei anzeigen

@@ -0,0 +1,43 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
import * as React from 'react';
import { mockAnalysisEvent } from '../../../../helpers/testMocks';
import { Events, EventsProps } from '../Events';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

function shallowRender(props: Partial<EventsProps> = {}) {
return shallow(
<Events
analysisKey="foo"
events={[
mockAnalysisEvent(),
mockAnalysisEvent({ category: 'VERSION' }),
mockAnalysisEvent({ category: 'OTHER' })
]}
onChange={jest.fn()}
onDelete={jest.fn()}
{...props}
/>
);
}

+ 78
- 46
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx Datei anzeigen

@@ -20,6 +20,8 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { parseDate } from 'sonar-ui-common/helpers/dates';
import { mockParsedAnalysis } from '../../../../helpers/testMocks';
import { ComponentQualifier } from '../../../../types/component';
import { DEFAULT_GRAPH } from '../../utils';
import ProjectActivityAnalysesList from '../ProjectActivityAnalysesList';

@@ -36,67 +38,97 @@ jest.mock('sonar-ui-common/helpers/dates', () => {

const DATE = parseDate('2016-10-27T16:33:50+0000');

const ANALYSES = [
{
key: 'A1',
date: DATE,
events: [{ key: 'E1', category: 'VERSION', name: '6.5-SNAPSHOT' }]
},
{ key: 'A2', date: parseDate('2016-10-27T12:21:15+0000'), events: [] },
{
key: 'A3',
date: parseDate('2016-10-26T12:17:29+0000'),
events: [
{ key: 'E2', category: 'VERSION', name: '6.4' },
{ key: 'E3', category: 'OTHER', name: 'foo' }
]
},
{
key: 'A4',
date: parseDate('2016-10-24T16:33:50+0000'),
events: [{ key: 'E1', category: 'QUALITY_GATE', name: 'Quality gate changed to red...' }]
}
];

const DEFAULT_PROPS: ProjectActivityAnalysesList['props'] = {
addCustomEvent: jest.fn().mockResolvedValue(undefined),
addVersion: jest.fn().mockResolvedValue(undefined),
analyses: ANALYSES,
analysesLoading: false,
canAdmin: false,
changeEvent: jest.fn().mockResolvedValue(undefined),
deleteAnalysis: jest.fn().mockResolvedValue(undefined),
deleteEvent: jest.fn().mockResolvedValue(undefined),
initializing: false,
leakPeriodDate: parseDate('2016-10-27T12:21:15+0000'),
project: { qualifier: 'TRK' },
query: {
category: '',
customMetrics: [],
graph: DEFAULT_GRAPH,
project: 'org.sonarsource.sonarqube:sonarqube'
},
updateQuery: () => {}
const DEFAULT_QUERY = {
category: '',
customMetrics: [],
graph: DEFAULT_GRAPH,
project: 'org.sonarsource.sonarqube:sonarqube'
};

it('should render correctly', () => {
expect(shallow(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />)).toMatchSnapshot();
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ project: { qualifier: ComponentQualifier.Application } })).toMatchSnapshot(
'application'
);
expect(shallowRender({ analyses: [], initializing: true })).toMatchSnapshot('loading');
expect(shallowRender({ analyses: [] })).toMatchSnapshot('no analyses');
});

it('should correctly filter analyses by category', () => {
const wrapper = shallow(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />);
wrapper.setProps({ query: { ...DEFAULT_PROPS.query, category: 'QUALITY_GATE' } });
const wrapper = shallowRender();
wrapper.setProps({ query: { ...DEFAULT_QUERY, category: 'QUALITY_GATE' } });
expect(wrapper).toMatchSnapshot();
});

it('should correctly filter analyses by date range', () => {
const wrapper = shallow(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />);
const wrapper = shallowRender();
wrapper.setProps({
query: {
...DEFAULT_PROPS.query,
...DEFAULT_QUERY,
from: DATE,
to: DATE
}
});
expect(wrapper).toMatchSnapshot();
});

it('should correctly update the selected date', () => {
const selectedDate = new Date();
const updateQuery = jest.fn();
const wrapper = shallowRender({ updateQuery });
wrapper.instance().updateSelectedDate(selectedDate);
expect(updateQuery).toBeCalledWith({ selectedDate });
});

it('should correctly reset scroll if filters change', () => {
const wrapper = shallowRender();
const scrollContainer = document.createElement('ul');
scrollContainer.scrollTop = 100;

// Saves us a call to mount().
wrapper.instance().scrollContainer = scrollContainer;

wrapper.setProps({ query: { ...DEFAULT_QUERY, category: 'OTHER' } });
expect(scrollContainer.scrollTop).toBe(0);
});

function shallowRender(props: Partial<ProjectActivityAnalysesList['props']> = {}) {
return shallow<ProjectActivityAnalysesList>(
<ProjectActivityAnalysesList
addCustomEvent={jest.fn().mockResolvedValue(undefined)}
addVersion={jest.fn().mockResolvedValue(undefined)}
analyses={[
mockParsedAnalysis({
key: 'A1',
date: DATE,
events: [{ key: 'E1', category: 'VERSION', name: '6.5-SNAPSHOT' }]
}),
mockParsedAnalysis({ key: 'A2', date: parseDate('2016-10-27T12:21:15+0000') }),
mockParsedAnalysis({
key: 'A3',
date: parseDate('2016-10-26T12:17:29+0000'),
events: [
{ key: 'E2', category: 'VERSION', name: '6.4' },
{ key: 'E3', category: 'OTHER', name: 'foo' }
]
}),
mockParsedAnalysis({
key: 'A4',
date: parseDate('2016-10-24T16:33:50+0000'),
events: [{ key: 'E1', category: 'QUALITY_GATE', name: 'Quality gate changed to red...' }]
})
]}
analysesLoading={false}
canAdmin={false}
changeEvent={jest.fn().mockResolvedValue(undefined)}
deleteAnalysis={jest.fn().mockResolvedValue(undefined)}
deleteEvent={jest.fn().mockResolvedValue(undefined)}
initializing={false}
leakPeriodDate={parseDate('2016-10-27T12:21:15+0000')}
project={{ qualifier: ComponentQualifier.Project }}
query={DEFAULT_QUERY}
updateQuery={jest.fn()}
{...props}
/>
);
}

+ 77
- 25
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx Datei anzeigen

@@ -17,11 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
/* eslint-disable sonarjs/no-duplicate-string */
import { mount, shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { IntlProvider } from 'react-intl';
import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
import { click } from 'sonar-ui-common/helpers/testUtils';
import TimeFormatter from '../../../../components/intl/TimeFormatter';
import { mockAnalysisEvent, mockParsedAnalysis } from '../../../../helpers/testMocks';
import ProjectActivityAnalysis from '../ProjectActivityAnalysis';
import AddEventForm from '../forms/AddEventForm';
import RemoveAnalysisForm from '../forms/RemoveAnalysisForm';
import { ProjectActivityAnalysis, ProjectActivityAnalysisProps } from '../ProjectActivityAnalysis';

jest.mock('sonar-ui-common/helpers/dates', () => ({
parseDate: () => ({
@@ -30,15 +36,35 @@ jest.mock('sonar-ui-common/helpers/dates', () => ({
})
}));

jest.mock('sonar-ui-common/helpers/scrolling', () => ({
scrollToElement: jest.fn()
}));

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender()).toMatchSnapshot('default');
expect(
shallowRender({ analysis: mockParsedAnalysis({ events: [mockAnalysisEvent()] }) })
).toMatchSnapshot();
).toMatchSnapshot('with events');
expect(
shallowRender({ analysis: mockParsedAnalysis({ buildString: '1.0.234' }) })
).toMatchSnapshot();
expect(shallowRender({ isBaseline: true })).toMatchSnapshot();
).toMatchSnapshot('with build string');
expect(shallowRender({ isBaseline: true })).toMatchSnapshot('with baseline marker');
expect(
shallowRender({
canAdmin: true,
canCreateVersion: true,
canDeleteAnalyses: true
})
).toMatchSnapshot('with admin options');

const timeFormatter = shallowRender()
.find(TimeFormatter)
.prop('children');
if (!timeFormatter) {
fail('TimeFormatter instance not found');
} else {
expect(timeFormatter('formatted_time')).toMatchSnapshot('formatted time');
}
});

it('should show the correct admin options', () => {
@@ -47,24 +73,27 @@ it('should show the correct admin options', () => {
canCreateVersion: true,
canDeleteAnalyses: true
});
const instance = wrapper.instance();

expect(wrapper).toMatchSnapshot();

instance.setState({ addEventForm: true });
waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
instance.setState({ addEventForm: false });
expect(wrapper.find('.js-add-version').exists()).toBe(true);
click(wrapper.find('.js-add-version'));
const addVersionForm = wrapper.find(AddEventForm);
expect(addVersionForm.exists()).toBe(true);
addVersionForm.prop('onClose')();
expect(wrapper.find(AddEventForm).exists()).toBe(false);

instance.setState({ removeAnalysisForm: true });
waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
instance.setState({ removeAnalysisForm: false });
expect(wrapper.find('.js-add-event').exists()).toBe(true);
click(wrapper.find('.js-add-event'));
const addEventForm = wrapper.find(AddEventForm);
expect(addEventForm.exists()).toBe(true);
addEventForm.prop('onClose')();
expect(wrapper.find(AddEventForm).exists()).toBe(false);

instance.setState({ addVersionForm: true });
waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
instance.setState({ addVersionForm: false });
expect(wrapper.find('.js-delete-analysis').exists()).toBe(true);
click(wrapper.find('.js-delete-analysis'));
const removeAnalysisForm = wrapper.find(RemoveAnalysisForm);
expect(removeAnalysisForm.exists()).toBe(true);
removeAnalysisForm.prop('onClose')();
expect(wrapper.find(RemoveAnalysisForm).exists()).toBe(false);
});

it('should not allow the first item to be deleted', () => {
@@ -75,11 +104,34 @@ it('should not allow the first item to be deleted', () => {
canDeleteAnalyses: true,
isFirst: true
})
).toMatchSnapshot();
.find('.js-delete-analysis')
.exists()
).toBe(false);
});

function shallowRender(props: Partial<ProjectActivityAnalysis['props']> = {}) {
return shallow(
it('should be clickable', () => {
const date = new Date('2018-03-01T09:37:01+0100');
const updateSelectedDate = jest.fn();
const wrapper = shallowRender({ analysis: mockParsedAnalysis({ date }), updateSelectedDate });
click(wrapper);
expect(updateSelectedDate).toBeCalledWith(date);
});

it('should trigger a scroll to itself if selected', () => {
mountRender({ parentScrollContainer: document.createElement('ul'), selected: true });
expect(scrollToElement).toBeCalled();
});

function shallowRender(props: Partial<ProjectActivityAnalysisProps> = {}) {
return shallow(createComponent(props));
}

function mountRender(props: Partial<ProjectActivityAnalysisProps> = {}) {
return mount(<IntlProvider locale="en">{createComponent(props)}</IntlProvider>);
}

function createComponent(props: Partial<ProjectActivityAnalysisProps> = {}) {
return (
<ProjectActivityAnalysis
addCustomEvent={jest.fn()}
addVersion={jest.fn()}

+ 30
- 60
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/DefinitionChangeEventInner-test.tsx.snap Datei anzeigen

@@ -1,24 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render 1`] = `
<div
className="project-activity-event-inner"
>
<div
className="project-activity-event-inner-main"
<Fragment>
<span
className="note"
>
<ProjectEventIcon
className="project-activity-event-icon little-spacer-right DEFINITION_CHANGE"
/>
<div
className="project-activity-event-inner-text flex-1"
>
<span
className="note little-spacer-right"
>
event.category.DEFINITION_CHANGE
</span>
</div>
event.category.DEFINITION_CHANGE
:
</span>
<div>
<ResetButtonLink
className="project-activity-event-inner-more-link"
onClick={[Function]}
@@ -31,28 +21,18 @@ exports[`should render 1`] = `
/>
</ResetButtonLink>
</div>
</div>
</Fragment>
`;

exports[`should render 2`] = `
<div
className="project-activity-event-inner"
>
<div
className="project-activity-event-inner-main"
<Fragment>
<span
className="note"
>
<ProjectEventIcon
className="project-activity-event-icon little-spacer-right DEFINITION_CHANGE"
/>
<div
className="project-activity-event-inner-text flex-1"
>
<span
className="note little-spacer-right"
>
event.category.DEFINITION_CHANGE
</span>
</div>
event.category.DEFINITION_CHANGE
:
</span>
<div>
<ResetButtonLink
className="project-activity-event-inner-more-link"
onClick={[Function]}
@@ -66,10 +46,10 @@ exports[`should render 2`] = `
</ResetButtonLink>
</div>
<ul
className="project-activity-event-inner-more-content"
className="spacer-left spacer-top"
>
<li
className="display-flex-center little-spacer-top"
className="display-flex-center spacer-top"
key="foo"
>
<div
@@ -112,7 +92,7 @@ exports[`should render 2`] = `
</div>
</li>
<li
className="display-flex-center little-spacer-top"
className="display-flex-center spacer-top"
key="bar"
>
<div
@@ -155,28 +135,18 @@ exports[`should render 2`] = `
</div>
</li>
</ul>
</div>
</Fragment>
`;

exports[`should render for a branch 1`] = `
<div
className="project-activity-event-inner"
>
<div
className="project-activity-event-inner-main"
<Fragment>
<span
className="note"
>
<ProjectEventIcon
className="project-activity-event-icon little-spacer-right DEFINITION_CHANGE"
/>
<div
className="project-activity-event-inner-text flex-1"
>
<span
className="note little-spacer-right"
>
event.category.DEFINITION_CHANGE
</span>
</div>
event.category.DEFINITION_CHANGE
:
</span>
<div>
<ResetButtonLink
className="project-activity-event-inner-more-link"
onClick={[Function]}
@@ -190,10 +160,10 @@ exports[`should render for a branch 1`] = `
</ResetButtonLink>
</div>
<ul
className="project-activity-event-inner-more-content"
className="spacer-left spacer-top"
>
<li
className="display-flex-center little-spacer-top"
className="display-flex-center spacer-top"
key="foo"
>
<div
@@ -236,7 +206,7 @@ exports[`should render for a branch 1`] = `
</div>
</li>
<li
className="display-flex-center little-spacer-top"
className="display-flex-center spacer-top"
key="bar"
>
<FormattedMessage
@@ -284,5 +254,5 @@ exports[`should render for a branch 1`] = `
/>
</li>
</ul>
</div>
</Fragment>
`;

+ 91
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Event-test.tsx.snap Datei anzeigen

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

exports[`should render correctly: default 1`] = `
<div
className="project-activity-event"
>
<ProjectEventIcon
className="project-activity-event-icon little-spacer-right text-middle OTHER"
/>
<EventInner
event={
Object {
"category": "OTHER",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
/>
</div>
`;

exports[`should render correctly: with admin options 1`] = `
<div
className="project-activity-event"
>
<ProjectEventIcon
className="project-activity-event-icon little-spacer-right text-middle OTHER"
/>
<EventInner
event={
Object {
"category": "OTHER",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
/>
<span
className="nowrap"
>
<EditButton
aria-label="project_activity.events.tooltip.edit"
className="button-small"
data-test="project-activity__edit-event"
onClick={[Function]}
stopPropagation={true}
/>
<DeleteButton
aria-label="project_activity.events.tooltip.delete"
className="button-small"
data-test="project-activity__delete-event"
onClick={[Function]}
stopPropagation={true}
/>
</span>
</div>
`;

+ 101
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/EventInner-test.tsx.snap Datei anzeigen

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

exports[`should render correctly: default 1`] = `
<Tooltip
overlay="Lorem ipsum dolor sit amet"
>
<span
className="text-middle"
>
<span
className="note little-spacer-right"
>
event.category.VERSION
:
</span>
<strong
className="spacer-right"
>
Lorem ipsum
</strong>
</span>
</Tooltip>
`;

exports[`should render correctly: definition change 1`] = `
<DefinitionChangeEventInner
branchLike={
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": false,
"name": "branch-6.7",
}
}
event={
Object {
"category": "DEFINITION_CHANGE",
"definitionChange": Object {
"projects": Array [
Object {
"changeType": "ADDED",
"key": "foo",
"name": "Foo",
},
],
},
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": undefined,
}
}
/>
`;

exports[`should render correctly: no description 1`] = `
<span
className="text-middle"
>
<span
className="note little-spacer-right"
>
event.category.VERSION
:
</span>
<strong
className="spacer-right"
>
Lorem ipsum
</strong>
</span>
`;

exports[`should render correctly: rich quality gate 1`] = `
<RichQualityGateEventInner
event={
Object {
"category": "QUALITY_GATE",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
/>
`;

+ 98
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Events-test.tsx.snap Datei anzeigen

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

exports[`should render correctly 1`] = `
<div
className="big-spacer-top"
>
<Memo(Event)
analysisKey="foo"
event={
Object {
"category": "OTHER",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
key="E11"
onChange={[MockFunction]}
onDelete={[MockFunction]}
/>
<Memo(Event)
analysisKey="foo"
event={
Object {
"category": "QUALITY_GATE",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
key="E11"
onChange={[MockFunction]}
onDelete={[MockFunction]}
/>
<Memo(Event)
analysisKey="foo"
event={
Object {
"category": "VERSION",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
}
}
key="E11"
onChange={[MockFunction]}
onDelete={[MockFunction]}
/>
</div>
`;

+ 261
- 7
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap Datei anzeigen

@@ -46,7 +46,7 @@ exports[`should correctly filter analyses by category 1`] = `
<ul
className="project-activity-analyses-list"
>
<ProjectActivityAnalysis
<Memo(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -60,6 +60,7 @@ exports[`should correctly filter analyses by category 1`] = `
},
],
"key": "A4",
"projectVersion": "1.0",
}
}
canAdmin={false}
@@ -126,7 +127,7 @@ exports[`should correctly filter analyses by date range 1`] = `
<ul
className="project-activity-analyses-list"
>
<ProjectActivityAnalysis
<Memo(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -140,6 +141,7 @@ exports[`should correctly filter analyses by date range 1`] = `
},
],
"key": "A1",
"projectVersion": "1.0",
}
}
canAdmin={false}
@@ -160,7 +162,229 @@ exports[`should correctly filter analyses by date range 1`] = `
</ul>
`;

exports[`should render correctly 1`] = `
exports[`should render correctly: application 1`] = `
<ul
className="project-activity-versions-list"
onScroll={[Function]}
style={
Object {
"paddingTop": undefined,
}
}
>
<li
key="E1"
>
<div
className="project-activity-version-badge first"
>
<Tooltip
mouseEnterDelay={0.5}
overlay="version 6.5-SNAPSHOT"
>
<span
className="analysis-version"
>
6.5-SNAPSHOT
</span>
</Tooltip>
</div>
<ul
className="project-activity-days-list"
>
<li
className="project-activity-day"
data-day="ISO.1477526400000"
key="1477526400000"
>
<div
className="project-activity-date"
>
<DateFormatter
date={1477526400000}
long={true}
/>
</div>
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
Object {
"date": 2016-10-27T16:33:50.000Z,
"events": Array [
Object {
"category": "VERSION",
"key": "E1",
"name": "6.5-SNAPSHOT",
},
],
"key": "A1",
"projectVersion": "1.0",
}
}
canAdmin={false}
canCreateVersion={false}
changeEvent={[MockFunction]}
deleteAnalysis={[MockFunction]}
deleteEvent={[MockFunction]}
isBaseline={false}
isFirst={true}
key="A1"
selected={false}
updateSelectedDate={[Function]}
/>
<Memo(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
Object {
"date": 2016-10-27T12:21:15.000Z,
"events": Array [],
"key": "A2",
"projectVersion": "1.0",
}
}
canAdmin={false}
canCreateVersion={false}
changeEvent={[MockFunction]}
deleteAnalysis={[MockFunction]}
deleteEvent={[MockFunction]}
isBaseline={true}
isFirst={false}
key="A2"
selected={false}
updateSelectedDate={[Function]}
/>
</ul>
</li>
</ul>
</li>
<li
key="E2"
>
<div
className="project-activity-version-badge"
>
<Tooltip
mouseEnterDelay={0.5}
overlay="version 6.4"
>
<span
className="analysis-version"
>
6.4
</span>
</Tooltip>
</div>
<ul
className="project-activity-days-list"
>
<li
className="project-activity-day"
data-day="ISO.1477440000000"
key="1477440000000"
>
<div
className="project-activity-date"
>
<DateFormatter
date={1477440000000}
long={true}
/>
</div>
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
Object {
"date": 2016-10-26T12:17:29.000Z,
"events": Array [
Object {
"category": "VERSION",
"key": "E2",
"name": "6.4",
},
Object {
"category": "OTHER",
"key": "E3",
"name": "foo",
},
],
"key": "A3",
"projectVersion": "1.0",
}
}
canAdmin={false}
canCreateVersion={false}
changeEvent={[MockFunction]}
deleteAnalysis={[MockFunction]}
deleteEvent={[MockFunction]}
isBaseline={false}
isFirst={false}
key="A3"
selected={false}
updateSelectedDate={[Function]}
/>
</ul>
</li>
<li
className="project-activity-day"
data-day="ISO.1477267200000"
key="1477267200000"
>
<div
className="project-activity-date"
>
<DateFormatter
date={1477267200000}
long={true}
/>
</div>
<ul
className="project-activity-analyses-list"
>
<Memo(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
Object {
"date": 2016-10-24T16:33:50.000Z,
"events": Array [
Object {
"category": "QUALITY_GATE",
"key": "E1",
"name": "Quality gate changed to red...",
},
],
"key": "A4",
"projectVersion": "1.0",
}
}
canAdmin={false}
canCreateVersion={false}
changeEvent={[MockFunction]}
deleteAnalysis={[MockFunction]}
deleteEvent={[MockFunction]}
isBaseline={false}
isFirst={false}
key="A4"
selected={false}
updateSelectedDate={[Function]}
/>
</ul>
</li>
</ul>
</li>
</ul>
`;

exports[`should render correctly: default 1`] = `
<ul
className="project-activity-versions-list"
onScroll={[Function]}
@@ -206,7 +430,7 @@ exports[`should render correctly 1`] = `
<ul
className="project-activity-analyses-list"
>
<ProjectActivityAnalysis
<Memo(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -220,6 +444,7 @@ exports[`should render correctly 1`] = `
},
],
"key": "A1",
"projectVersion": "1.0",
}
}
canAdmin={false}
@@ -233,7 +458,7 @@ exports[`should render correctly 1`] = `
selected={false}
updateSelectedDate={[Function]}
/>
<ProjectActivityAnalysis
<Memo(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -241,6 +466,7 @@ exports[`should render correctly 1`] = `
"date": 2016-10-27T12:21:15.000Z,
"events": Array [],
"key": "A2",
"projectVersion": "1.0",
}
}
canAdmin={false}
@@ -294,7 +520,7 @@ exports[`should render correctly 1`] = `
<ul
className="project-activity-analyses-list"
>
<ProjectActivityAnalysis
<Memo(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -313,6 +539,7 @@ exports[`should render correctly 1`] = `
},
],
"key": "A3",
"projectVersion": "1.0",
}
}
canAdmin={false}
@@ -344,7 +571,7 @@ exports[`should render correctly 1`] = `
<ul
className="project-activity-analyses-list"
>
<ProjectActivityAnalysis
<Memo(ProjectActivityAnalysis)
addCustomEvent={[MockFunction]}
addVersion={[MockFunction]}
analysis={
@@ -358,6 +585,7 @@ exports[`should render correctly 1`] = `
},
],
"key": "A4",
"projectVersion": "1.0",
}
}
canAdmin={false}
@@ -377,3 +605,29 @@ exports[`should render correctly 1`] = `
</li>
</ul>
`;

exports[`should render correctly: loading 1`] = `
<div
className="boxed-group-inner"
>
<div
className="text-center"
>
<i
className="spinner"
/>
</div>
</div>
`;

exports[`should render correctly: no analyses 1`] = `
<div
className="boxed-group-inner"
>
<span
className="note"
>
no_results
</span>
</div>
`;

+ 126
- 493
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap Datei anzeigen

@@ -1,132 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should not allow the first item to be deleted 1`] = `
<Tooltip
mouseEnterDelay={0.5}
overlay={
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={true}
/>
}
placement="left"
>
<li
className="project-activity-analysis"
data-date={1546333200000}
onClick={[Function]}
tabIndex={0}
>
<div
className="project-activity-time spacer-right"
>
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={false}
>
<Component />
</TimeFormatter>
</div>
<div
className="project-activity-analysis-actions big-spacer-right"
>
<ActionsDropdown
small={true}
toggleClassName="js-analysis-actions"
>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_version
</ActionsDropdownItem>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_custom_event
</ActionsDropdownItem>
</ActionsDropdown>
</div>
</li>
</Tooltip>
`;

exports[`should render correctly 1`] = `
<Tooltip
mouseEnterDelay={0.5}
overlay={
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={true}
/>
}
placement="left"
>
<li
className="project-activity-analysis"
data-date={1546333200000}
onClick={[Function]}
tabIndex={0}
>
<div
className="project-activity-time spacer-right"
>
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={false}
>
<Component />
</TimeFormatter>
</div>
</li>
</Tooltip>
`;

exports[`should render correctly 2`] = `
<Tooltip
mouseEnterDelay={0.5}
overlay={
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={true}
/>
}
placement="left"
exports[`should render correctly: default 1`] = `
<li
className="project-activity-analysis bordered-top bordered-bottom"
onClick={[Function]}
>
<li
className="project-activity-analysis"
data-date={1546333200000}
onClick={[Function]}
tabIndex={0}
<div
className="display-flex-center display-flex-space-between"
>
<div
className="project-activity-time spacer-right"
className="project-activity-time"
>
<TimeFormatter
date={
@@ -140,111 +23,29 @@ exports[`should render correctly 2`] = `
<Component />
</TimeFormatter>
</div>
<Events
analysis="foo"
changeEvent={[MockFunction]}
deleteEvent={[MockFunction]}
events={
Array [
Object {
"category": "QUALITY_GATE",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
},
]
}
isFirst={false}
/>
</li>
</Tooltip>
</div>
</li>
`;

exports[`should render correctly 3`] = `
<Tooltip
mouseEnterDelay={0.5}
overlay={
<React.Fragment>
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={true}
/>
<br />
project_activity.analysis_build_string_X.1.0.234
</React.Fragment>
}
placement="left"
exports[`should render correctly: formatted time 1`] = `
<time
className="text-middle"
dateTime="2019-01-01T09:00:00.000Z"
>
<li
className="project-activity-analysis"
data-date={1546333200000}
onClick={[Function]}
tabIndex={0}
>
<div
className="project-activity-time spacer-right"
>
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={false}
>
<Component />
</TimeFormatter>
</div>
</li>
</Tooltip>
formatted_time
</time>
`;

exports[`should render correctly 4`] = `
<Tooltip
mouseEnterDelay={0.5}
overlay={
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={true}
/>
}
placement="left"
exports[`should render correctly: with admin options 1`] = `
<li
className="project-activity-analysis bordered-top bordered-bottom"
onClick={[Function]}
>
<li
className="project-activity-analysis"
data-date={1546333200000}
onClick={[Function]}
tabIndex={0}
<div
className="display-flex-center display-flex-space-between"
>
<div
className="project-activity-time spacer-right"
className="project-activity-time"
>
<TimeFormatter
date={
@@ -258,52 +59,52 @@ exports[`should render correctly 4`] = `
<Component />
</TimeFormatter>
</div>
<div
className="baseline-marker"
>
<ClickEventBoundary>
<div
className="wedge"
/>
<hr />
<div
className="label display-flex-center"
className="project-activity-analysis-actions big-spacer-left"
>
project_activity.new_code_period_start
<HelpTooltip
className="little-spacer-left"
overlay="project_activity.new_code_period_start.help"
placement="top"
/>
<ActionsDropdown
overlayPlacement="bottom-right"
small={true}
toggleClassName="js-analysis-actions"
>
<ActionsDropdownItem
className="js-add-version"
onClick={[Function]}
>
project_activity.add_version
</ActionsDropdownItem>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_custom_event
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem
className="js-delete-analysis"
destructive={true}
onClick={[Function]}
>
project_activity.delete_analysis
</ActionsDropdownItem>
</ActionsDropdown>
</div>
</div>
</li>
</Tooltip>
</ClickEventBoundary>
</div>
</li>
`;

exports[`should show the correct admin options 1`] = `
<Tooltip
mouseEnterDelay={0.5}
overlay={
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={true}
/>
}
placement="left"
exports[`should render correctly: with baseline marker 1`] = `
<li
className="project-activity-analysis bordered-top bordered-bottom"
onClick={[Function]}
>
<li
className="project-activity-analysis"
data-date={1546333200000}
onClick={[Function]}
tabIndex={0}
<div
className="display-flex-center display-flex-space-between"
>
<div
className="project-activity-time spacer-right"
className="project-activity-time"
>
<TimeFormatter
date={
@@ -317,146 +118,38 @@ exports[`should show the correct admin options 1`] = `
<Component />
</TimeFormatter>
</div>
<div
className="project-activity-analysis-actions big-spacer-right"
>
<ActionsDropdown
small={true}
toggleClassName="js-analysis-actions"
>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_version
</ActionsDropdownItem>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_custom_event
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem
className="js-delete-analysis"
destructive={true}
onClick={[Function]}
>
project_activity.delete_analysis
</ActionsDropdownItem>
</ActionsDropdown>
</div>
</li>
</Tooltip>
`;

exports[`should show the correct admin options 2`] = `
<Tooltip
mouseEnterDelay={0.5}
overlay={
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={true}
/>
}
placement="left"
>
<li
className="project-activity-analysis"
data-date={1546333200000}
onClick={[Function]}
tabIndex={0}
</div>
<div
className="baseline-marker"
>
<div
className="project-activity-time spacer-right"
>
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={false}
>
<Component />
</TimeFormatter>
</div>
className="wedge"
/>
<hr />
<div
className="project-activity-analysis-actions big-spacer-right"
className="label display-flex-center"
>
<ActionsDropdown
small={true}
toggleClassName="js-analysis-actions"
>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_version
</ActionsDropdownItem>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_custom_event
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem
className="js-delete-analysis"
destructive={true}
onClick={[Function]}
>
project_activity.delete_analysis
</ActionsDropdownItem>
</ActionsDropdown>
<AddEventForm
addEvent={[MockFunction]}
addEventButtonText="project_activity.add_custom_event"
analysis={
Object {
"date": 2017-03-01T08:37:01.000Z,
"events": Array [],
"key": "foo",
"projectVersion": "1.0",
}
}
onClose={[Function]}
project_activity.new_code_period_start
<HelpTooltip
className="little-spacer-left"
overlay="project_activity.new_code_period_start.help"
placement="top"
/>
</div>
</li>
</Tooltip>
</div>
</li>
`;

exports[`should show the correct admin options 3`] = `
<Tooltip
mouseEnterDelay={0.5}
overlay={
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={true}
/>
}
placement="left"
exports[`should render correctly: with build string 1`] = `
<li
className="project-activity-analysis bordered-top bordered-bottom"
onClick={[Function]}
>
<li
className="project-activity-analysis"
data-date={1546333200000}
onClick={[Function]}
tabIndex={0}
<div
className="display-flex-center display-flex-space-between"
>
<div
className="project-activity-time spacer-right"
className="project-activity-time"
>
<TimeFormatter
date={
@@ -471,74 +164,24 @@ exports[`should show the correct admin options 3`] = `
</TimeFormatter>
</div>
<div
className="project-activity-analysis-actions big-spacer-right"
className="flex-shrink small text-muted text-ellipsis"
>
<ActionsDropdown
small={true}
toggleClassName="js-analysis-actions"
>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_version
</ActionsDropdownItem>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_custom_event
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem
className="js-delete-analysis"
destructive={true}
onClick={[Function]}
>
project_activity.delete_analysis
</ActionsDropdownItem>
</ActionsDropdown>
<RemoveAnalysisForm
analysis={
Object {
"date": 2017-03-01T08:37:01.000Z,
"events": Array [],
"key": "foo",
"projectVersion": "1.0",
}
}
deleteAnalysis={[MockFunction]}
onClose={[Function]}
/>
project_activity.analysis_build_string_X.1.0.234
</div>
</li>
</Tooltip>
</div>
</li>
`;

exports[`should show the correct admin options 4`] = `
<Tooltip
mouseEnterDelay={0.5}
overlay={
<TimeFormatter
date={
Object {
"toISOString": [Function],
"valueOf": [Function],
}
}
long={true}
/>
}
placement="left"
exports[`should render correctly: with events 1`] = `
<li
className="project-activity-analysis bordered-top bordered-bottom"
onClick={[Function]}
>
<li
className="project-activity-analysis"
data-date={1546333200000}
onClick={[Function]}
tabIndex={0}
<div
className="display-flex-center display-flex-space-between"
>
<div
className="project-activity-time spacer-right"
className="project-activity-time"
>
<TimeFormatter
date={
@@ -552,48 +195,38 @@ exports[`should show the correct admin options 4`] = `
<Component />
</TimeFormatter>
</div>
<div
className="project-activity-analysis-actions big-spacer-right"
>
<ActionsDropdown
small={true}
toggleClassName="js-analysis-actions"
>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_version
</ActionsDropdownItem>
<ActionsDropdownItem
className="js-add-event"
onClick={[Function]}
>
project_activity.add_custom_event
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem
className="js-delete-analysis"
destructive={true}
onClick={[Function]}
>
project_activity.delete_analysis
</ActionsDropdownItem>
</ActionsDropdown>
<AddEventForm
addEvent={[MockFunction]}
addEventButtonText="project_activity.add_version"
analysis={
Object {
"date": 2017-03-01T08:37:01.000Z,
"events": Array [],
"key": "foo",
"projectVersion": "1.0",
}
}
onClose={[Function]}
/>
</div>
</li>
</Tooltip>
</div>
<Memo(Events)
analysisKey="foo"
events={
Array [
Object {
"category": "QUALITY_GATE",
"description": "Lorem ipsum dolor sit amet",
"key": "E11",
"name": "Lorem ipsum",
"qualityGate": Object {
"failing": Array [
Object {
"branch": "master",
"key": "foo",
"name": "Foo",
},
Object {
"branch": "feature/bar",
"key": "bar",
"name": "Bar",
},
],
"status": "ERROR",
"stillFailing": true,
},
},
]
}
isFirst={false}
onChange={[MockFunction]}
onDelete={[MockFunction]}
/>
</li>
`;

+ 0
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap Datei anzeigen

@@ -76,7 +76,6 @@ exports[`should render correctly 1`] = `
canAdmin={false}
canDeleteAnalyses={false}
changeEvent={[MockFunction]}
className="boxed-group-inner"
deleteAnalysis={[MockFunction]}
deleteEvent={[MockFunction]}
initializing={false}

+ 45
- 67
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap Datei anzeigen

@@ -1,37 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render 1`] = `
<div
className="project-activity-event-inner"
>
<div
className="project-activity-event-inner-main"
<Fragment>
<span
className="note spacer-right"
>
<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>
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]}
@@ -44,41 +33,30 @@ exports[`should render 1`] = `
/>
</ResetButtonLink>
</div>
</div>
</Fragment>
`;

exports[`should render 2`] = `
<div
className="project-activity-event-inner"
>
<div
className="project-activity-event-inner-main"
<Fragment>
<span
className="note spacer-right"
>
<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>
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]}
@@ -92,14 +70,14 @@ exports[`should render 2`] = `
</ResetButtonLink>
</div>
<ul
className="project-activity-event-inner-more-content"
className="spacer-left spacer-top"
>
<li
className="display-flex-center little-spacer-top"
className="display-flex-center spacer-top"
key="foo"
>
<Level
className="little-spacer-right"
className="spacer-right"
level="ERROR"
small={true}
/>
@@ -126,11 +104,11 @@ exports[`should render 2`] = `
</div>
</li>
<li
className="display-flex-center little-spacer-top"
className="display-flex-center spacer-top"
key="bar"
>
<Level
className="little-spacer-right"
className="spacer-right"
level="ERROR"
small={true}
/>
@@ -157,5 +135,5 @@ exports[`should render 2`] = `
</div>
</li>
</ul>
</div>
</Fragment>
`;

+ 15
- 20
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.tsx Datei anzeigen

@@ -21,30 +21,25 @@ import * as React from 'react';
import ConfirmModal from 'sonar-ui-common/components/controls/ConfirmModal';
import { translate } from 'sonar-ui-common/helpers/l10n';

interface Props {
analysis: string;
deleteEvent: (analysis: string, event: string) => Promise<void>;
export interface RemoveEventFormProps {
analysisKey: string;
event: T.AnalysisEvent;
header: string;
removeEventQuestion: string;
onClose: () => void;
onConfirm: (analysis: string, event: string) => Promise<void>;
}

export default class RemoveEventForm extends React.PureComponent<Props> {
handleSubmit = () => {
return this.props.deleteEvent(this.props.analysis, this.props.event.key);
};

render() {
return (
<ConfirmModal
confirmButtonText={translate('delete')}
header={this.props.header}
isDestructive={true}
onClose={this.props.onClose}
onConfirm={this.handleSubmit}>
{translate(this.props.removeEventQuestion)}
</ConfirmModal>
);
}
export default function RemoveEventForm(props: RemoveEventFormProps) {
const { analysisKey, event, header, removeEventQuestion } = props;
return (
<ConfirmModal
confirmButtonText={translate('delete')}
header={header}
isDestructive={true}
onClose={props.onClose}
onConfirm={() => props.onConfirm(analysisKey, event.key)}>
{removeEventQuestion}
</ConfirmModal>
);
}

+ 56
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/RemoveEventForm-test.tsx Datei anzeigen

@@ -0,0 +1,56 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
import * as React from 'react';
import ConfirmModal from 'sonar-ui-common/components/controls/ConfirmModal';
import { mockAnalysisEvent } from '../../../../../helpers/testMocks';
import RemoveEventForm, { RemoveEventFormProps } from '../RemoveEventForm';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should correctly confirm', () => {
const onConfirm = jest.fn();
const wrapper = shallowRender({ onConfirm });
wrapper.find(ConfirmModal).prop('onConfirm')();
expect(onConfirm).toBeCalledWith('foo', 'bar');
});

it('should correctly cancel', () => {
const onClose = jest.fn();
const wrapper = shallowRender({ onClose });
wrapper.find(ConfirmModal).prop('onClose')();
expect(onClose).toBeCalled();
});

function shallowRender(props: Partial<RemoveEventFormProps> = {}) {
return shallow(
<RemoveEventForm
analysisKey="foo"
event={mockAnalysisEvent({ key: 'bar' })}
header="Remove foo"
onClose={jest.fn()}
onConfirm={jest.fn()}
removeEventQuestion="Remove foo?"
{...props}
/>
);
}

+ 13
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/RemoveEventForm-test.tsx.snap Datei anzeigen

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

exports[`should render correctly 1`] = `
<ConfirmModal
confirmButtonText="delete"
header="Remove foo"
isDestructive={true}
onClose={[MockFunction]}
onConfirm={[Function]}
>
Remove foo?
</ConfirmModal>
`;

+ 15
- 63
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css Datei anzeigen

@@ -31,11 +31,6 @@
align-items: stretch;
}

.project-activity-page-side-outer > .boxed-group-inner {
padding-left: 12px;
padding-right: 15px;
}

.project-activity-layout-page-main {
flex-grow: 1;
min-width: 640px;
@@ -59,6 +54,8 @@
overflow: auto;
flex-grow: 1;
flex-shrink: 0;
padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) calc(2 * var(--gridSize))
calc(1.5 * var(--gridSize));
}

.project-activity-day {
@@ -71,23 +68,21 @@
}

.project-activity-date {
margin-bottom: 16px;
font-size: 15px;
margin-bottom: calc(2 * var(--gridSize));
font-size: var(--bigFontSize);
font-weight: bold;
}

.project-activity-analysis {
position: relative;
display: flex;
min-height: var(--smallControlHeight);
padding: var(--gridSize) calc(0.5 * var(--gridSize));
border-top: 1px solid var(--barBorderColor);
border-bottom: 1px solid var(--barBorderColor);
padding: calc(2 * var(--gridSize));
cursor: pointer;
}

.project-activity-analysis.selected {
background-color: #ecf6fe;
cursor: default;
}

.project-activity-analysis:focus {
@@ -103,69 +98,26 @@
}

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

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

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

.project-activity-event {
line-height: var(--smallControlHeight);
display: flex;
text-indent: -20px;
padding-left: 20px;
}

.project-activity-event + .project-activity-event {
margin-top: 4px;
}

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

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

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

.project-activity-event-inner-text {
line-height: var(--smallControlHeight);
margin-top: var(--gridSize);
}

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

.project-activity-event-inner-more-content {
margin-left: 18px;
}

.project-activity-event-actions {
flex-shrink: 0;
flex-grow: 0;
}

.project-activity-event-icon.VERSION {
@@ -189,9 +141,9 @@
}

.project-activity-version-badge {
margin-left: -12px;
padding-top: 8px;
padding-bottom: 8px;
margin-left: calc(-1.5 * var(--gridSize));
padding-top: var(--gridSize);
padding-bottom: var(--gridSize);
background-color: white;
}

@@ -199,9 +151,9 @@
.project-activity-version-badge.first {
position: absolute;
top: 0;
left: 12px;
right: 16px;
padding-top: 24px;
left: calc(1.5 * var(--gridSize));
right: calc(2 * var(--gridSize));
padding-top: calc(3 * var(--gridSize));
z-index: var(--belowNormalZIndex);
}


+ 1
- 3
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx Datei anzeigen

@@ -247,9 +247,7 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State

{analysis.events.length > 0 && (
<Events
analysis={analysis.key}
changeEvent={() => Promise.resolve()}
deleteEvent={() => Promise.resolve()}
analysisKey={analysis.key}
events={analysis.events}
isFirst={analyses[0].key === analysis.key}
/>

Laden…
Abbrechen
Speichern