Skip to content

Commit

Permalink
feat(core/managed): switch to abbreviated timestamp format, add versi…
Browse files Browse the repository at this point in the history
…on timestamps (#8610)

* feat(core/managed): add RelativeTimestamp component

* feat(core/managed): show createdAt timestamp on versions

* feat(core/managed): use RelativeTimestamp for environment cards

* feat(core/managed): add click to copy on created timestamp

* feat(core/managed): add AbsoluteTimestamp component, use in ArtifactDetail

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
Erik Munson and mergify[bot] committed Oct 1, 2020
1 parent 012b004 commit 34bc7e9
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 68 deletions.
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/domain/IManagedEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export interface IManagedEnvironmentSummary {
export interface IManagedArtifactVersion {
version: string;
displayName: string;
createdAt?: string;
environments: Array<{
name: string;
state: 'current' | 'deploying' | 'approved' | 'pending' | 'previous' | 'vetoed' | 'skipped';
Expand Down
33 changes: 33 additions & 0 deletions app/scripts/modules/core/src/managed/AbsoluteTimestamp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { memo } from 'react';
import { DateTime } from 'luxon';

import { SETTINGS } from '../config';
import { CopyToClipboard } from '../utils';

export interface IAbsoluteTimestampProps {
timestamp: DateTime;
clickToCopy?: boolean;
}

const TIMEZONE = SETTINGS.feature.displayTimestampsInUserLocalTime ? undefined : SETTINGS.defaultTimeZone;

export const AbsoluteTimestamp = memo(
({ timestamp: timestampInOriginalZone, clickToCopy }: IAbsoluteTimestampProps) => {
const timestamp = timestampInOriginalZone.setZone(TIMEZONE);

const fullTimestamp = timestamp.toFormat('yyyy-MM-dd HH:mm:ss ZZZZ');
const formattedTimestamp = timestamp.toFormat('MMM d, y HH:mm');
const timestampElement = <span>{formattedTimestamp}</span>;

if (clickToCopy) {
return (
<span>
{timestampElement}
<CopyToClipboard text={fullTimestamp} toolTip={`${fullTimestamp} (click to copy)`} />
</span>
);
} else {
return timestampElement;
}
},
);
19 changes: 12 additions & 7 deletions app/scripts/modules/core/src/managed/ArtifactDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useRouter } from '@uirouter/react';
import { useTransition, animated, UseTransitionProps } from 'react-spring';
import { DateTime } from 'luxon';

import { relativeTime, timestamp } from '../utils';
import {
IManagedArtifactSummary,
IManagedArtifactVersion,
Expand All @@ -14,6 +13,7 @@ import {
import { Application } from '../application';
import { useEventListener, Markdown } from '../presentation';

import { AbsoluteTimestamp } from './AbsoluteTimestamp';
import { ArtifactDetailHeader } from './ArtifactDetailHeader';
import { ManagedResourceObject } from './ManagedResourceObject';
import { EnvironmentRow } from './EnvironmentRow';
Expand Down Expand Up @@ -94,8 +94,6 @@ const EnvironmentCards = memo(
stateService: { go },
} = useRouter();

const pinnedAtMillis = pinned?.at ? DateTime.fromISO(pinned.at).toMillis() : null;

const differentVersionPinnedCard = pinnedVersion &&
pinnedVersion !== versionDetails.version &&
!['vetoed', 'skipped'].includes(state) && (
Expand All @@ -113,11 +111,11 @@ const EnvironmentCards = memo(
iconName="pin"
appearance="warning"
background={true}
timestamp={pinned?.at ? DateTime.fromISO(pinned.at) : null}
title={
<span className="sp-group-margin-xs-xaxis">
Pinned here {relativeTime(pinnedAtMillis)}{' '}
<span className="text-italic text-regular sp-margin-xs-left">({timestamp(pinnedAtMillis)})</span>{' '}
<span className="text-regular"></span> <span className="text-regular">by {pinned.by}</span>
<span>Pinned</span> <span className="text-regular"></span>{' '}
<span className="text-regular">by {pinned.by}</span>
</span>
}
description={pinned.comment && <Markdown message={pinned.comment} tag="span" />}
Expand Down Expand Up @@ -218,7 +216,7 @@ export const ArtifactDetail = ({
resourcesByEnvironment,
onRequestClose,
}: IArtifactDetailProps) => {
const { environments, git } = versionDetails;
const { environments, git, createdAt } = versionDetails;

const keydownCallback = ({ keyCode }: KeyboardEvent) => {
if (keyCode === 27 /* esc */) {
Expand All @@ -229,6 +227,7 @@ export const ArtifactDetail = ({

const isPinnedEverywhere = environments.every(({ pinned }) => pinned);
const isBadEverywhere = environments.every(({ state }) => state === 'vetoed');
const createdAtTimestamp = useMemo(() => createdAt && DateTime.fromISO(createdAt), [createdAt]);

return (
<>
Expand Down Expand Up @@ -266,6 +265,12 @@ export const ArtifactDetail = ({
</Button>
</div>
<div className="detail-section-right flex-container-v flex-pull-right sp-margin-l-right">
{createdAtTimestamp && (
<VersionMetadataItem
label="Created"
value={<AbsoluteTimestamp timestamp={createdAtTimestamp} clickToCopy={true} />}
/>
)}
{git?.author && <VersionMetadataItem label="Author" value={git.author} />}
{git?.pullRequest?.number && git?.pullRequest?.url && (
<VersionMetadataItem
Expand Down
10 changes: 7 additions & 3 deletions app/scripts/modules/core/src/managed/ArtifactsList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useMemo, useState } from 'react';
import classNames from 'classnames';
import React, { useState } from 'react';
import { DateTime } from 'luxon';

import {
IManagedArtifactSummary,
Expand All @@ -13,6 +14,7 @@ import { isConstraintSupported, getConstraintIcon } from './constraints/constrai

import { ISelectedArtifactVersion } from './Environments';
import { Pill } from './Pill';
import { RelativeTimestamp } from './RelativeTimestamp';
import { IStatusBubbleStackProps, StatusBubbleStack } from './StatusBubbleStack';

import './ArtifactRow.less';
Expand Down Expand Up @@ -73,11 +75,12 @@ interface IArtifactRowProps {
}

export const ArtifactRow = ({ isSelected, clickHandler, version: versionInfo, reference, name }: IArtifactRowProps) => {
const { version, displayName, environments, build, git } = versionInfo;
const { version, displayName, createdAt, environments, build, git } = versionInfo;
const [isHovered, setIsHovered] = useState(false);

const versionIcon = getVersionIcon(versionInfo);
const secondarySummary = getVersionSecondarySummary(versionInfo);
const timestamp = useMemo(() => createdAt && DateTime.fromISO(createdAt), [createdAt]);

return (
<div
Expand All @@ -88,8 +91,9 @@ export const ArtifactRow = ({ isSelected, clickHandler, version: versionInfo, re
>
<div className="row-content flex-container-v left sp-padding-m-top sp-padding-l-bottom sp-padding-s-xaxis">
{(build?.number || build?.id) && (
<div className="flex-container-h sp-margin-s-bottom">
<div className="row-middle-section flex-container-h space-between middle sp-margin-s-bottom">
<Pill bgColor={isSelected ? '#2e4b5f' : undefined} text={`#${build.number || build.id} ${name || ''}`} />
{timestamp && <RelativeTimestamp timestamp={timestamp} />}
</div>
)}
<div className="row-middle-section flex-container-h space-between">
Expand Down
75 changes: 75 additions & 0 deletions app/scripts/modules/core/src/managed/RelativeTimestamp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { memo, useEffect, useState } from 'react';
import { DateTime, Duration } from 'luxon';

import { SETTINGS } from '../config';
import { CopyToClipboard } from '../utils';
import { useInterval, Tooltip } from '../presentation';

export interface IRelativeTimestampProps {
timestamp: DateTime;
clickToCopy?: boolean;
}

const TIMEZONE = SETTINGS.feature.displayTimestampsInUserLocalTime ? undefined : SETTINGS.defaultTimeZone;

const formatTimestamp = (timestamp: DateTime, distance: Duration) => {
if (distance.years || distance.months) {
if (timestamp.year === DateTime.local().setZone(TIMEZONE).year) {
return timestamp.toFormat('MMM d');
} else {
return timestamp.toFormat('MMM d, y');
}
} else if (distance.days) {
return distance.toFormat('d') + 'd';
} else if (distance.hours) {
return distance.toFormat('h') + 'h';
} else if (distance.minutes) {
return distance.toFormat('m') + 'm';
} else if (distance.seconds) {
return distance.toFormat('s') + 's';
} else {
return null;
}
};

const getDistanceFromNow = (timestamp: DateTime) =>
timestamp.diffNow().negate().shiftTo('years', 'months', 'days', 'hours', 'minutes', 'seconds');

export const RelativeTimestamp = memo(
({ timestamp: timestampInOriginalZone, clickToCopy }: IRelativeTimestampProps) => {
const timestamp = timestampInOriginalZone.setZone(TIMEZONE);
const [formattedTimestamp, setFormattedTimestamp] = useState(
formatTimestamp(timestamp, getDistanceFromNow(timestamp)),
);

const updateTimestamp = () => {
setFormattedTimestamp(formatTimestamp(timestamp, getDistanceFromNow(timestamp)));
};

useInterval(updateTimestamp, 1000);
useEffect(updateTimestamp, [timestamp]);

if (!formattedTimestamp) {
return null;
}

const absoluteTimestamp = timestamp.toFormat('yyyy-MM-dd HH:mm:ss ZZZZ');
const relativeTimestamp = (
<span className="text-regular text-italic" style={{ fontSize: 13, lineHeight: 1 }}>
{formattedTimestamp}
</span>
);

if (clickToCopy) {
return (
<CopyToClipboard
buttonInnerNode={relativeTimestamp}
text={absoluteTimestamp}
toolTip={`${absoluteTimestamp} (click to copy)`}
/>
);
} else {
return <Tooltip value={absoluteTimestamp}>{relativeTimestamp}</Tooltip>;
}
},
);
16 changes: 15 additions & 1 deletion app/scripts/modules/core/src/managed/StatusCard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import classNames from 'classnames';
import { DateTime } from 'luxon';

import { IconNames } from '../presentation';

import { StatusBubble } from './StatusBubble';
import { RelativeTimestamp } from './RelativeTimestamp';

import './StatusCard.less';

Expand All @@ -12,11 +14,20 @@ export interface IStatusCardProps {
background?: boolean;
iconName: IconNames;
title: React.ReactNode;
timestamp?: DateTime;
description?: React.ReactNode;
actions?: React.ReactNode;
}

export const StatusCard = ({ appearance, background, iconName, title, description, actions }: IStatusCardProps) => (
export const StatusCard = ({
appearance,
background,
iconName,
title,
timestamp,
description,
actions,
}: IStatusCardProps) => (
<div
className={classNames(
'StatusCard flex-container-h space-between middle wrap sp-padding-s-yaxis sp-padding-l-xaxis',
Expand All @@ -28,6 +39,9 @@ export const StatusCard = ({ appearance, background, iconName, title, descriptio
<div className="flex-container-h center middle sp-margin-l-right">
<StatusBubble iconName={iconName} appearance={appearance} size="medium" />
</div>
<div className="sp-margin-m-right" style={{ minWidth: 24 }}>
{timestamp && <RelativeTimestamp timestamp={timestamp} clickToCopy={true} />}
</div>
<div className="flex-container-v sp-margin-xs-yaxis">
<div className="text-bold">{title}</div>
{description && <div className="text-regular">{description}</div>}
Expand Down
Loading

0 comments on commit 34bc7e9

Please sign in to comment.