]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12637 Improve activity list
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 19 Dec 2019 16:35:59 +0000 (17:35 +0100)
committerSonarTech <sonartech@sonarsource.com>
Thu, 2 Jan 2020 19:46:12 +0000 (20:46 +0100)
27 files changed:
server/sonar-web/src/main/js/app/styles/init/misc.css
server/sonar-web/src/main/js/apps/projectActivity/components/DefinitionChangeEventInner.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/RichQualityGateEventInner.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Event-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/EventInner-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/Events-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysis-test.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/DefinitionChangeEventInner-test.tsx.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Event-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/EventInner-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/Events-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysis-test.tsx.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/RichQualityGateEventInner-test.tsx.snap
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/RemoveEventForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/RemoveEventForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx

index bd3ba06d26f2a58eaa613a47e8945b0fd2655d07..4f62e454230864c4698c8e5ed6c42b2154fd26b0 100644 (file)
@@ -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;
index abae92b45b1dca6ad548c4300c1478c43a3e1b0e..184e45c6a1d9a3ce237c8e5a4bb2121c722bc89f 100644 (file)
  * 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>
+      </>
     );
   }
 }
index b295b7af807df16bc93f511639b7061cf7c2f03f..9e4593a237d2c6e0553b582d23e31561cc7f2f8b 100644 (file)
  * 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);
index 0472b32916e4621ce65db969ceddb81ef51f27e7..b4bdae8d5f0f2f8de66a39d7392268fc40d85d23 100644 (file)
  * 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;
   }
 }
index 84e418c7527b5450b6e3ce625581f852c2815a82..cea19eced71fb3a10cebc66660131c6adfd9ea29 100644 (file)
@@ -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);
index ff168b9903257f92b1ffcb700a361b8bdf33d094..4e65a83d6a78385b843fe527ffe627736c055617 100644 (file)
@@ -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) {
index cc748a3041cff815f1d109c1c531118414c02f44..ff49a472342409aac3297b31132c3519a08ed91e 100644 (file)
@@ -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);
index fef42d2314d40ad3991de3a32e086b7586d0a1e0..6376019b7b59b403bcd19f88ac972bfe7718ea12 100644 (file)
@@ -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}
index 0f6849554009904e97fd901e14c913af08bfa128..7213e3860aaaceeca4c0cb9a50a0f8d74f2a1ebe 100644 (file)
  * 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>
+      </>
     );
   }
 }
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 (file)
index 0000000..301df23
--- /dev/null
@@ -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}
+    />
+  );
+}
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 (file)
index 0000000..ecdd5a9
--- /dev/null
@@ -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}
+    />
+  );
+}
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 (file)
index 0000000..d4671c4
--- /dev/null
@@ -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}
+    />
+  );
+}
index 4a40545a1e4f6aa85105aabf14adb7cfd87a7522..de053f1f2a053f79d47ce6505066827852fa8a04 100644 (file)
@@ -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}
+    />
+  );
+}
index 175ea87e3433f49406745fae6da07a5516d7bf83..5ffe98fae8ba6e88575bd8a8d3532863c0704f2e 100644 (file)
  * 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()}
index 04895e1a18a1649b83373811f368f27045c9350b..e4c0559e8cd2ee29149ceaa08104b179975e71c9 100644 (file)
@@ -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>
 `;
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 (file)
index 0000000..d3e5f56
--- /dev/null
@@ -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>
+`;
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 (file)
index 0000000..cd7db05
--- /dev/null
@@ -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,
+      },
+    }
+  }
+/>
+`;
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 (file)
index 0000000..8eceec7
--- /dev/null
@@ -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>
+`;
index 37ab95a8e9ef416e148fda61bea5d95634a9cdfc..1f18e89399fa8c33f9f96ad83391384211831fb2 100644 (file)
@@ -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>
+`;
index 6e8e9a74b2ac9cae8d3d774c4b0d5fe862baeace..60ab7cbfe78ac13206f7d1d8cbe12d4eb6782ba5 100644 (file)
 // 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>
 `;
index 635b6c84a4c0e84bfda6e4d7f56ed8d5f5f6f32c..caa6cdf253c8f93aa071863f49181ee6ac5d8f2d 100644 (file)
@@ -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}
index 0ee3134b1314c949fa91b1eef7a2341208376935..8e7bee851da42902620f32f36800c1e750abb06a 100644 (file)
@@ -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>
 `;
index 07be3232557ee681af1815ff33bb94af3aa2a4dd..507f0314d783ce9a37f469b74151d85e03724636 100644 (file)
@@ -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>
+  );
 }
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 (file)
index 0000000..9f9db01
--- /dev/null
@@ -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}
+    />
+  );
+}
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 (file)
index 0000000..8d69aad
--- /dev/null
@@ -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>
+`;
index 01fc828ec5f988d6bc9316fd226d9358d859beeb..32dc163cef967269933324bb28c987198f5723e1 100644 (file)
   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 {
 }
 
 .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 {
 }
 
 .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 {
 }
 
 .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;
 }
 
 .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);
 }
 
index 04bb16887821f1c563565095fdd5ae7948720365..44095aef493e33134003df82b236360336f851f3 100644 (file)
@@ -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}
                                       />