Skip to content

Commit

Permalink
feat(core/managed): surface pre-deployment steps for versions (#8750)
Browse files Browse the repository at this point in the history
* feat(core/presentation): add bake icon

* feat(core/domain): add lifecycle steps to managed artifact versions

* feat(core/managed): show status bubble for baking on versions

* feat(core/managed): surface baking on version detail pane

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
Erik Munson and mergify[bot] committed Nov 23, 2020
1 parent 0cdf836 commit f4d7d14
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 2 deletions.
11 changes: 11 additions & 0 deletions app/scripts/modules/core/src/domain/IManagedEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ export interface IManagedArtifactVersion {
statelessConstraints?: IStatelessConstraint[];
compareLink?: string;
}>;
lifecycleSteps?: Array<{
// likely more scopes + types later, but hard-coding to avoid premature abstraction for now
scope: 'PRE_DEPLOYMENT';
type: 'BAKE';
id: string;
status: 'NOT_STARTED' | 'RUNNING' | 'SUCCEEDED' | 'FAILED';
startedAt?: string;
completedAt?: string;
link?: string;
}>;
build?: {
id: number; // deprecated, use number
number: string;
Expand Down Expand Up @@ -139,6 +149,7 @@ export interface IManagedArtifactVersion {
}

export type IManagedArtifactVersionEnvironment = IManagedArtifactSummary['versions'][0]['environments'][0];
export type IManagedArtifactVersionLifecycleStep = IManagedArtifactSummary['versions'][0]['lifecycleSteps'][0];

export interface IManagedArtifactSummary {
name: string;
Expand Down
12 changes: 11 additions & 1 deletion app/scripts/modules/core/src/managed/ArtifactDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { AbsoluteTimestamp } from './AbsoluteTimestamp';
import { ArtifactDetailHeader } from './ArtifactDetailHeader';
import { ManagedResourceObject } from './ManagedResourceObject';
import { EnvironmentRow } from './EnvironmentRow';
import { PreDeploymentRow } from './PreDeploymentRow';
import { PreDeploymentStepCard } from './PreDeploymentStepCard';
import { VersionStateCard } from './VersionStateCard';
import { StatusCard } from './StatusCard';
import { Button } from './Button';
Expand Down Expand Up @@ -230,7 +232,7 @@ export const ArtifactDetail = ({
resourcesByEnvironment,
onRequestClose,
}: IArtifactDetailProps) => {
const { environments, git, createdAt } = versionDetails;
const { environments, lifecycleSteps, git, createdAt } = versionDetails;

const keydownCallback = ({ keyCode }: KeyboardEvent) => {
if (keyCode === 27 /* esc */) {
Expand All @@ -242,6 +244,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]);
const preDeploymentSteps = lifecycleSteps?.filter(({ scope }) => scope === 'PRE_DEPLOYMENT');

return (
<>
Expand Down Expand Up @@ -384,6 +387,13 @@ export const ArtifactDetail = ({
</EnvironmentRow>
);
})}
{preDeploymentSteps && preDeploymentSteps.length > 0 && (
<PreDeploymentRow>
{lifecycleSteps.map((step) => (
<PreDeploymentStepCard key={step.id} step={step} application={application} reference={reference} />
))}
</PreDeploymentRow>
)}
</div>
</>
);
Expand Down
11 changes: 10 additions & 1 deletion app/scripts/modules/core/src/managed/ArtifactsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,20 @@ export const ArtifactRow = ({ isSelected, clickHandler, version: versionInfo, re
};

type ArtifactStatusList = IStatusBubbleStackProps['statuses'];
function getArtifactStatuses({ environments }: IManagedArtifactVersion): ArtifactStatusList {
function getArtifactStatuses({ environments, lifecycleSteps }: IManagedArtifactVersion): ArtifactStatusList {
const statuses: ArtifactStatusList = [];
// NOTE: The order in which entries are added to `statuses` is important. The highest priority
// item must be inserted first.

const bakeStep = lifecycleSteps?.find(
({ scope, type, status }) =>
scope === 'PRE_DEPLOYMENT' && type === 'BAKE' && ['RUNNING', 'FAILED'].includes(status),
);

if (bakeStep) {
statuses.push({ iconName: 'bake', appearance: bakeStep.status === 'RUNNING' ? 'progress' : 'error' });
}

const pendingConstraintIcons = new Set<IconNames>();
const failedConstraintIcons = new Set<IconNames>();

Expand Down
17 changes: 17 additions & 0 deletions app/scripts/modules/core/src/managed/PreDeploymentRow.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.PreDeploymentRow {
max-width: 100%;

.srow {
display: flex;
align-items: center;
box-shadow: inset 0 -1px 0 0 var(--color-nobel);
height: 44px;
font-size: 16px;
color: var(--color-nobel);
padding-left: 8px;
}

.titleColumn {
flex-basis: 50%;
}
}
19 changes: 19 additions & 0 deletions app/scripts/modules/core/src/managed/PreDeploymentRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';

import './PreDeploymentRow.less';

interface IPreDeploymentRowProps {
children?: React.ReactNode;
}

export function PreDeploymentRow({ children }: IPreDeploymentRowProps) {
return (
<div className="PreDeploymentRow">
<div className="srow">
<div className="titleColumn text-bold flex-container-h left middle sp-margin-s-right">PRE-DEPLOYMENT</div>
</div>

<div style={{ margin: '16px 0 40px 8px' }}>{children}</div>
</div>
);
}
96 changes: 96 additions & 0 deletions app/scripts/modules/core/src/managed/PreDeploymentStepCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { memo } from 'react';
import ReactGA from 'react-ga';
import { DateTime } from 'luxon';
import * as distanceInWords from 'date-fns/distance_in_words';

import { IManagedArtifactVersionLifecycleStep } from '../domain';
import { Application } from '../application';

import { StatusCard } from './StatusCard';
import { Button } from './Button';

const SUPPORTED_TYPES = ['BAKE'];

const cardConfigurationByType = {
BAKE: {
iconName: 'bake',
appearance: {
NOT_STARTED: 'future',
RUNNING: 'info',
SUCCEEDED: 'neutral',
FAILED: 'error',
},
title: ({ status, startedAt, completedAt }: IManagedArtifactVersionLifecycleStep) => {
const startedAtDate = startedAt ? DateTime.fromISO(startedAt).toJSDate() : null;
const completedAtDate = completedAt ? DateTime.fromISO(completedAt).toJSDate() : null;

switch (status) {
case 'NOT_STARTED':
return 'An image will be baked before deployment';
case 'RUNNING':
return 'Baking';
case 'SUCCEEDED':
return `Baked in ${distanceInWords(startedAtDate, completedAtDate)}`;
case 'FAILED':
return `Baking failed after ${distanceInWords(startedAtDate, completedAtDate)}`;
default:
return null;
}
},
},
} as const;

const logEvent = (label: string, application: string, reference: string, type: string, status: string) =>
ReactGA.event({
category: 'Environments - version details',
action: label,
label: `${application}:${reference}:${type}:${status}`,
});

const getTimestamp = (startedAt: string, completedAt: string) => {
if (completedAt) {
return DateTime.fromISO(completedAt);
} else if (startedAt) {
return DateTime.fromISO(startedAt);
} else {
return null;
}
};

export interface PreDeploymentStepCardProps {
step: IManagedArtifactVersionLifecycleStep;
application: Application;
reference: string;
}

export const PreDeploymentStepCard = memo(({ step, application, reference }: PreDeploymentStepCardProps) => {
const { type, status, startedAt, completedAt, link } = step;

if (!SUPPORTED_TYPES.includes(type)) {
return null;
}

const { iconName, appearance, title } = cardConfigurationByType[type];

return (
<StatusCard
appearance={appearance[status]}
active={status !== 'NOT_STARTED'}
iconName={iconName}
timestamp={getTimestamp(startedAt, completedAt)}
title={title(step)}
actions={
link && (
<Button
onClick={() => {
window.open(link, '_blank', 'noopener noreferrer');
logEvent('Pre-deployment details link clicked', application.name, reference, type, status);
}}
>
See {status === 'RUNNING' ? 'progress' : 'details'}
</Button>
)
}
/>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ReactComponent as artifactApproved } from './vectors/artifactApproved.s
import { ReactComponent as artifactBad } from './vectors/artifactBad.svg';
import { ReactComponent as artifactPending } from './vectors/artifactPending.svg';
import { ReactComponent as artifactSkipped } from './vectors/artifactSkipped.svg';
import { ReactComponent as bake } from './vectors/bake.svg';
import { ReactComponent as build } from './vectors/build.svg';
import { ReactComponent as buildFail } from './vectors/buildFail.svg';
import { ReactComponent as buildSuccess } from './vectors/buildSuccess.svg';
Expand Down Expand Up @@ -128,6 +129,7 @@ export const iconsByName = {
artifactBad,
artifactPending,
artifactSkipped,
bake,
build,
buildFail,
buildSuccess,
Expand Down
34 changes: 34 additions & 0 deletions app/scripts/modules/core/src/presentation/icons/vectors/bake.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f4d7d14

Please sign in to comment.