From: Wouter Admiraal Date: Thu, 19 Dec 2019 16:35:59 +0000 (+0100) Subject: SONAR-12637 Improve activity list X-Git-Tag: 8.2.0.32929~244 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=9a63cf197276d41e7b390bb4a96ef6e71ac38344;p=sonarqube.git SONAR-12637 Improve activity list --- diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index bd3ba06d26f..4f62e454230 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -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; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx index abae92b45b1..184e45c6a1d 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx @@ -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 -
- - -
- - {translate('event.category', event.category)} - -
+ <> + {translate('event.category', event.category)}: +
{expanded && ( -
    +
      {event.definitionChange.projects.map(project => ( -
    • +
    • {this.renderProjectChange(project)}
    • ))}
    )} -
+ ); } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx index b295b7af807..9e4593a237d 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx @@ -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; - deleteEvent: (analysis: string, event: string) => Promise; event: T.AnalysisEvent; isFirst?: boolean; + onChange?: (event: string, name: string) => Promise; + onDelete?: (analysisKey: string, event: string) => Promise; } -interface State { - changing: boolean; - deleting: boolean; -} - -export default class Event extends React.PureComponent { - 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 ( +
+ - return ( -
- + - {showActions && ( -
- {canChange && ( - - - - )} - {canDelete && ( - - - - )} -
- )} + {showActions && ( + + {canChange && ( + setChanging(true)} + stopPropagation={true} + /> + )} + {canDelete && ( + setDeleting(true)} + stopPropagation={true} + /> + )} + + )} - {this.state.changing && ( - - )} + {changing && props.onChange && ( + setChanging(false)} + /> + )} - {this.state.deleting && ( - - )} -
- ); - } + {deleting && props.onDelete && ( + setDeleting(false)} + onConfirm={props.onDelete} + removeEventQuestion={translate( + `project_activity.${isVersion ? 'remove_version' : 'remove_custom_event'}.question` + )} + /> + )} +
+ ); } + +export default React.memo(Event); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx index 0472b32916e..b4bdae8d5f0 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx @@ -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 ; } else if (isDefinitionChangeEvent(event)) { @@ -39,25 +38,14 @@ export default function EventInner({ event }: Props) { ); } else { - return ( -
-
- - - - - {translate('event.category', event.category)}: - - {event.name} - -
-
+ const content = ( + + + {translate('event.category', event.category)}: + + {event.name} + ); + return event.description ? {content} : content; } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx index 84e418c7527..cea19eced71 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx @@ -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; - deleteEvent: (analysis: string, event: string) => Promise; events: T.AnalysisEvent[]; isFirst?: boolean; + onChange?: (event: string, name: string) => Promise; + onDelete?: (analysis: string, event: string) => Promise; } -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 ( -
+
{sortedEvents.map(event => ( ))}
); } + +export default React.memo(Events); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx index ff168b99032..4e65a83d6a7 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx @@ -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; - className?: string; deleteAnalysis: (analysis: string) => Promise; deleteEvent: (analysis: string, event: string) => Promise; initializing: boolean; @@ -54,7 +49,7 @@ interface Props { export default class ProjectActivityAnalysesList extends React.PureComponent { analyses?: HTMLCollectionOf; badges?: HTMLCollectionOf; - scrollContainer?: HTMLElement | null; + scrollContainer?: HTMLUListElement | null; constructor(props: Props) { super(props); @@ -74,13 +69,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent { - 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 @@ -194,7 +166,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent 0); if (this.props.analyses.length === 0 || !hasFilteredData) { return ( -
+
{this.props.initializing ? (
@@ -208,10 +180,12 @@ export default class ProjectActivityAnalysesList extends React.PureComponent (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) { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx index cc748a3041c..ff49a472342 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx @@ -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; addVersion: (analysis: string, version: string) => Promise; analysis: T.ParsedAnalysis; @@ -44,141 +46,93 @@ interface Props { deleteEvent: (analysis: string, event: string) => Promise; 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 { - 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 ( -
-
-
-
- {translate('project_activity.new_code_period_start')} - + }); + + 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 ( +
  • props.updateSelectedDate(analysis.date)} + ref={ref => (node = ref)}> +
    +
    + + {formattedTime => ( + + )} +
    -
    - ); - } - - 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 = ; - if (analysis.buildString) { - tooltipContent = ( - <> - {tooltipContent} -
    - {translateWithParameters( - 'project_activity.analysis_build_string_X', - analysis.buildString - )} - - ); - } - return ( - -
  • -
    - - {formattedTime => ( - - )} - + {analysis.buildString && ( +
    + {translateWithParameters( + 'project_activity.analysis_build_string_X', + analysis.buildString + )}
    - - {(canAddVersion || canAddEvent || canDeleteAnalyses) && ( -
    - + )} + + {(canAddVersion || canAddEvent || canDeleteAnalyses) && ( + +
    + {canAddVersion && ( + className="js-add-version" + onClick={() => setAddVersionForm(true)}> {translate('project_activity.add_version')} )} {canAddEvent && ( - + setAddEventForm(true)}> {translate('project_activity.add_custom_event')} )} @@ -187,54 +141,69 @@ export default class ProjectActivityAnalysis extends React.PureComponent + onClick={() => setRemoveAnalysisForm(true)}> {translate('project_activity.delete_analysis')} )} - {this.state.addVersionForm && ( + {addVersionForm && ( setAddVersionForm(false)} /> )} - {this.state.addEventForm && ( + {addEventForm && ( setAddEventForm(false)} /> )} - {this.state.removeAnalysisForm && ( + {removeAnalysisForm && ( setRemoveAnalysisForm(false)} /> )}
    - )} - - {events.length > 0 && ( - - )} +
    + )} +
    - {isBaseline && this.renderBaselineMarker()} -
  • - - ); - } + {analysis.events.length > 0 && ( + + )} + + {isBaseline && ( +
    +
    +
    +
    + {translate('project_activity.new_code_period_start')} + +
    +
    + )} + + ); } + +export default React.memo(ProjectActivityAnalysis); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx index fef42d2314d..6376019b7b5 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx index 0f684955400..7213e3860aa 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx @@ -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 const { event } = this.props; const { expanded } = this.state; return ( -
    -
    - + {translate('event.category', event.category)}: + {event.qualityGate.stillFailing ? ( + }} /> + ) : ( + + )} -
    - - {translate('event.category', event.category)}: - - {event.qualityGate.stillFailing ? ( - }} - /> - ) : ( - - )} -
    - +
    {event.qualityGate.failing.length > 0 && (
    {expanded && ( -
      +
        {event.qualityGate.failing.map(project => ( -
      • - +
      • +
        ))}
      )} -
    + ); } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Event-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Event-test.tsx new file mode 100644 index 00000000000..301df23e68d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Event-test.tsx @@ -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 = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/EventInner-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/EventInner-test.tsx new file mode 100644 index 00000000000..ecdd5a95fe9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/EventInner-test.tsx @@ -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 = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Events-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Events-test.tsx new file mode 100644 index 00000000000..d4671c4be18 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Events-test.tsx @@ -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 = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx index 4a40545a1e4..de053f1f2a0 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx @@ -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()).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(); - 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(); + 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 = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx index 175ea87e343..5ffe98fae8b 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx @@ -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 = {}) { - 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 = {}) { + return shallow(createComponent(props)); +} + +function mountRender(props: Partial = {}) { + return mount({createComponent(props)}); +} + +function createComponent(props: Partial = {}) { + return ( -
    + - -
    - - event.category.DEFINITION_CHANGE - -
    + event.category.DEFINITION_CHANGE + : +
    +
    -
    + `; exports[`should render 2`] = ` -
    -
    + - -
    - - event.category.DEFINITION_CHANGE - -
    + event.category.DEFINITION_CHANGE + : +
    +
    -
    + `; exports[`should render for a branch 1`] = ` -
    -
    + - -
    - - event.category.DEFINITION_CHANGE - -
    + event.category.DEFINITION_CHANGE + : +
    +
    -
    + `; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Event-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Event-test.tsx.snap new file mode 100644 index 00000000000..d3e5f562c6e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Event-test.tsx.snap @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +
    + + +
    +`; + +exports[`should render correctly: with admin options 1`] = ` +
    + + + + + + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/EventInner-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/EventInner-test.tsx.snap new file mode 100644 index 00000000000..cd7db05bd52 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/EventInner-test.tsx.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` + + + + event.category.VERSION + : + + + Lorem ipsum + + + +`; + +exports[`should render correctly: definition change 1`] = ` + +`; + +exports[`should render correctly: no description 1`] = ` + + + event.category.VERSION + : + + + Lorem ipsum + + +`; + +exports[`should render correctly: rich quality gate 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Events-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Events-test.tsx.snap new file mode 100644 index 00000000000..8eceec7faa8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Events-test.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +
    + + + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap index 37ab95a8e9e..1f18e89399f 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap @@ -46,7 +46,7 @@ exports[`should correctly filter analyses by category 1`] = `
      - - `; -exports[`should render correctly 1`] = ` +exports[`should render correctly: application 1`] = ` +
        +
      • +
        + + + 6.5-SNAPSHOT + + +
        +
          +
        • +
          + +
          +
            + + +
          +
        • +
        +
      • +
      • +
        + + + 6.4 + + +
        +
          +
        • +
          + +
          +
            + +
          +
        • +
        • +
          + +
          +
            + +
          +
        • +
        +
      • +
      +`; + +exports[`should render correctly: default 1`] = `
        - - - -
      `; + +exports[`should render correctly: loading 1`] = ` +
      +
      + +
      +
      +`; + +exports[`should render correctly: no analyses 1`] = ` +
      + + no_results + +
      +`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap index 6e8e9a74b2a..60ab7cbfe78 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap @@ -1,132 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should not allow the first item to be deleted 1`] = ` - - } - placement="left" -> -
    • -
      - - - -
      -
      - - - project_activity.add_version - - - project_activity.add_custom_event - - -
      -
    • -
      -`; - -exports[`should render correctly 1`] = ` - - } - placement="left" -> -
    • -
      - - - -
      -
    • -
      -`; - -exports[`should render correctly 2`] = ` - - } - placement="left" +exports[`should render correctly: default 1`] = ` +
    • -
    • - -
    • -
      +
    + `; -exports[`should render correctly 3`] = ` - - -
    - project_activity.analysis_build_string_X.1.0.234 - - } - placement="left" +exports[`should render correctly: formatted time 1`] = ` +
    + formatted_time + `; -exports[`should render correctly 4`] = ` - - } - placement="left" +exports[`should render correctly: with admin options 1`] = ` +
  • -
  • -
    +
    -
    -
    - project_activity.new_code_period_start - + + + project_activity.add_version + + + project_activity.add_custom_event + + + + project_activity.delete_analysis + +
    -
    -
  • -
    + +
    + `; -exports[`should show the correct admin options 1`] = ` - - } - placement="left" +exports[`should render correctly: with baseline marker 1`] = ` +
  • -
  • -
    - - - project_activity.add_version - - - project_activity.add_custom_event - - - - project_activity.delete_analysis - - -
    -
  • -
    -`; - -exports[`should show the correct admin options 2`] = ` - - } - placement="left" -> -
  • +
    - - - -
    + className="wedge" + /> +
    - - - project_activity.add_version - - - project_activity.add_custom_event - - - - project_activity.delete_analysis - - -
    -
  • -
    +
    + `; -exports[`should show the correct admin options 3`] = ` - - } - placement="left" +exports[`should render correctly: with build string 1`] = ` +
  • -
  • - - - project_activity.add_version - - - project_activity.add_custom_event - - - - project_activity.delete_analysis - - - + project_activity.analysis_build_string_X.1.0.234
    -
  • -
    +
    + `; -exports[`should show the correct admin options 4`] = ` - - } - placement="left" +exports[`should render correctly: with events 1`] = ` +
  • -
  • -
    - - - project_activity.add_version - - - project_activity.add_custom_event - - - - project_activity.delete_analysis - - - -
    -
  • -
    +
    + + `; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap index 635b6c84a4c..caa6cdf253c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap @@ -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} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap index 0ee3134b131..8e7bee851da 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap @@ -1,37 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render 1`] = ` -
    -
    + - -
    - - event.category.QUALITY_GATE - : - - , - } - } - /> -
    + event.category.QUALITY_GATE + : +
    + , + } + } + /> +
    -
    + `; exports[`should render 2`] = ` -
    -
    + - -
    - - event.category.QUALITY_GATE - : - - , - } - } - /> -
    + event.category.QUALITY_GATE + : +
    + , + } + } + /> +
    • @@ -126,11 +104,11 @@ exports[`should render 2`] = `
  • @@ -157,5 +135,5 @@ exports[`should render 2`] = `
  • -
    + `; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.tsx index 07be3232557..507f0314d78 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.tsx @@ -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; +export interface RemoveEventFormProps { + analysisKey: string; event: T.AnalysisEvent; header: string; removeEventQuestion: string; onClose: () => void; + onConfirm: (analysis: string, event: string) => Promise; } -export default class RemoveEventForm extends React.PureComponent { - handleSubmit = () => { - return this.props.deleteEvent(this.props.analysis, this.props.event.key); - }; - - render() { - return ( - - {translate(this.props.removeEventQuestion)} - - ); - } +export default function RemoveEventForm(props: RemoveEventFormProps) { + const { analysisKey, event, header, removeEventQuestion } = props; + return ( + props.onConfirm(analysisKey, event.key)}> + {removeEventQuestion} + + ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/RemoveEventForm-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/RemoveEventForm-test.tsx new file mode 100644 index 00000000000..9f9db01291b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/RemoveEventForm-test.tsx @@ -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 = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/RemoveEventForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/RemoveEventForm-test.tsx.snap new file mode 100644 index 00000000000..8d69aad167c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/RemoveEventForm-test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + Remove foo? + +`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css index 01fc828ec5f..32dc163cef9 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css @@ -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); } diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx index 04bb1688782..44095aef493 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx @@ -247,9 +247,7 @@ export default class BranchAnalysisList extends React.PureComponent 0 && ( Promise.resolve()} - deleteEvent={() => Promise.resolve()} + analysisKey={analysis.key} events={analysis.events} isFirst={analyses[0].key === analysis.key} />