Skip to content

Commit

Permalink
feat(core/managed): add stateless constraint support (#8131)
Browse files Browse the repository at this point in the history
* feat(core/presentation): add constraint icons

* feat(core/managed): add stateless constraint support
  • Loading branch information
Erik Munson committed Apr 6, 2020
1 parent ab5d120 commit 0ee1888
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 56 deletions.
12 changes: 11 additions & 1 deletion app/scripts/modules/core/src/domain/IManagedEntity.ts
Expand Up @@ -30,6 +30,15 @@ export interface IStatefulConstraint {
comment?: string;
}

export interface IDependsOnConstraint {
type: 'depends-on';
currentlyPassing: boolean;
attributes: { environment: string };
}

// more stateless types coming soon
export type IStatelessConstraint = IDependsOnConstraint;

export interface IManagedResourceSummary {
id: string;
kind: string;
Expand Down Expand Up @@ -72,7 +81,8 @@ export interface IManagedArtifactVersion {
deployedAt?: string;
replacedAt?: string;
replacedBy?: string;
statefulConstraints: IStatefulConstraint[];
statefulConstraints?: IStatefulConstraint[];
statelessConstraints?: IStatelessConstraint[];
}>;
}

Expand Down
117 changes: 77 additions & 40 deletions app/scripts/modules/core/src/managed/ArtifactDetail.tsx
@@ -1,9 +1,9 @@
import React from 'react';
import React, { memo } from 'react';
import classNames from 'classnames';
import { DateTime } from 'luxon';

import { relativeTime, timestamp } from '../utils';
import { IManagedArtifactVersion, IManagedResourceSummary } from '../domain';
import { IManagedArtifactVersion, IManagedResourceSummary, IStatefulConstraint, IStatelessConstraint } from '../domain';
import { Application } from '../application';
import { useEventListener } from '../presentation';

Expand All @@ -13,7 +13,7 @@ import { Pill } from './Pill';
import { ManagedResourceObject } from './ManagedResourceObject';
import { parseName } from './Frigga';
import { EnvironmentRow } from './EnvironmentRow';
import { ConstraintCard } from './constraints/ConstraintCard';
import { ConstraintCard, IConstraintCardProps } from './constraints/ConstraintCard';
import { isConstraintSupported } from './constraints/constraintRegistry';

import './ArtifactDetail.less';
Expand All @@ -23,6 +23,30 @@ function shouldDisplayResource(name: string, type: string, resource: IManagedRes
return !!resource.moniker && name === resource.artifact?.name && type === resource.artifact?.type;
}

const ConstraintCards = memo(
({
constraints,
application,
environment,
version,
}: Partial<IConstraintCardProps> & { constraints: Array<IStatefulConstraint | IStatelessConstraint> }) => (
<>
{constraints
.filter(({ type }) => isConstraintSupported(type))
.map(constraint => (
<ConstraintCard
key={constraint.type}
className="sp-margin-l-right"
application={application}
environment={environment}
version={version}
constraint={constraint}
/>
))}
</>
),
);

export interface IArtifactDetailProps {
application: Application;
name: string;
Expand Down Expand Up @@ -57,7 +81,15 @@ export const ArtifactDetail = ({
<div className="detail-section-right">{/* artifact metadata will live here */}</div>
</div>
{environments.map(
({ name: environmentName, state, deployedAt, replacedAt, replacedBy, statefulConstraints }) => {
({
name: environmentName,
state,
deployedAt,
replacedAt,
replacedBy,
statefulConstraints,
statelessConstraints,
}) => {
const deployedAtMillis = DateTime.fromISO(deployedAt).toMillis();
const replacedAtMillis = DateTime.fromISO(replacedAt).toMillis();
const { version: replacedByPackageVersion, buildNumber: replacedByBuildNumber } =
Expand All @@ -69,22 +101,9 @@ export const ArtifactDetail = ({
name={environmentName}
resources={resourcesByEnvironment[environmentName]}
>
{statefulConstraints &&
statefulConstraints
.filter(({ type }) => isConstraintSupported(type))
.map(constraint => (
<ConstraintCard
key={constraint.type}
className="sp-margin-l-right"
application={application}
environment={environmentName}
version={version}
constraint={constraint}
/>
))}
{state === 'deploying' && (
<NoticeCard
className="sp-margin-l-right sp-margin-l-bottom"
className="sp-margin-l-right"
icon="cloudProgress"
text={undefined}
title="Deploying"
Expand All @@ -94,7 +113,7 @@ export const ArtifactDetail = ({
)}
{state === 'current' && deployedAt && (
<NoticeCard
className="sp-margin-l-right sp-margin-l-bottom"
className="sp-margin-l-right"
icon="cloudDeployed"
text={undefined}
title={
Expand All @@ -111,7 +130,7 @@ export const ArtifactDetail = ({
)}
{state === 'previous' && (
<NoticeCard
className="sp-margin-l-right sp-margin-l-bottom"
className="sp-margin-l-right"
icon="cloudDecommissioned"
text={undefined}
title={
Expand All @@ -134,7 +153,7 @@ export const ArtifactDetail = ({
)}
{state === 'pending' && (
<NoticeCard
className="sp-margin-l-right sp-margin-l-bottom"
className="sp-margin-l-right"
icon="placeholder"
text={undefined}
title="Never deployed here"
Expand All @@ -144,7 +163,7 @@ export const ArtifactDetail = ({
)}
{state === 'vetoed' && (
<NoticeCard
className="sp-margin-l-right sp-margin-l-bottom"
className="sp-margin-l-right"
icon="cloudError"
text={undefined}
title={
Expand All @@ -164,25 +183,43 @@ export const ArtifactDetail = ({
noticeType="error"
/>
)}
{resourcesByEnvironment[environmentName]
.filter(resource => shouldDisplayResource(name, type, resource))
.map(resource => (
<div key={resource.id} className="flex-container-h middle">
{state === 'deploying' && (
<div
className={classNames(
'resource-badge flex-container-h center middle sp-margin-s-right',
state,
)}
{statefulConstraints && (
<ConstraintCards
constraints={statefulConstraints}
application={application}
environment={environmentName}
version={version}
/>
)}
{statelessConstraints && (
<ConstraintCards
constraints={statelessConstraints}
application={application}
environment={environmentName}
version={version}
/>
)}
<div className="sp-margin-l-top">
{resourcesByEnvironment[environmentName]
.filter(resource => shouldDisplayResource(name, type, resource))
.map(resource => (
<div key={resource.id} className="flex-container-h middle">
{state === 'deploying' && (
<div
className={classNames(
'resource-badge flex-container-h center middle sp-margin-s-right',
state,
)}
/>
)}
<ManagedResourceObject
key={resource.id}
resource={resource}
depth={state === 'deploying' ? 0 : 1}
/>
)}
<ManagedResourceObject
key={resource.id}
resource={resource}
depth={state === 'deploying' ? 0 : 1}
/>
</div>
))}
</div>
))}
</div>
</EnvironmentRow>
);
},
Expand Down
@@ -1,13 +1,24 @@
import React, { useState } from 'react';
import classNames from 'classnames';

import { IStatefulConstraint, StatefulConstraintStatus, IManagedApplicationEnvironmentSummary } from '../../domain';
import {
IStatefulConstraint,
StatefulConstraintStatus,
IStatelessConstraint,
IManagedApplicationEnvironmentSummary,
} from '../../domain';
import { Application, ApplicationDataSource } from '../../application';
import { IRequestStatus } from '../../presentation';

import { NoticeCard } from '../NoticeCard';
import { ManagedWriter, IUpdateConstraintStatusRequest } from '../ManagedWriter';
import { isConstraintSupported, getStatefulConstraintConfig, getStatefulConstraintActions } from './constraintRegistry';
import {
isConstraintSupported,
isConstraintStateful,
getConstraintIcon,
getConstraintSummary,
getConstraintActions,
} from './constraintRegistry';

import './ConstraintCard.less';

Expand Down Expand Up @@ -47,16 +58,26 @@ const overrideConstraintStatus = (
return dataSource.refresh().catch(() => null);
});

const getCardAppearance = (constraint: IStatefulConstraint | IStatelessConstraint) => {
if (isConstraintStateful(constraint)) {
const { status } = constraint as IStatefulConstraint;
return constraintCardAppearanceByStatus[status];
} else {
const { currentlyPassing } = constraint as IStatelessConstraint;
return currentlyPassing ? 'success' : 'neutral';
}
};

export interface IConstraintCardProps {
application: Application;
environment: string;
version: string;
constraint: IStatefulConstraint;
constraint: IStatefulConstraint | IStatelessConstraint;
className?: string;
}

export const ConstraintCard = ({ application, environment, version, constraint, className }: IConstraintCardProps) => {
const { type, status } = constraint;
const { type } = constraint;

const [actionStatus, setActionStatus] = useState<IRequestStatus>('NONE');

Expand All @@ -65,13 +86,12 @@ export const ConstraintCard = ({ application, environment, version, constraint,
return null;
}

const { iconName, shortSummary } = getStatefulConstraintConfig(type);
const actions = getStatefulConstraintActions(constraint);
const actions = getConstraintActions(constraint);

return (
<NoticeCard
className={classNames('ConstraintCard', className)}
icon={iconName}
icon={getConstraintIcon(type)}
actions={
actions && (
<div
Expand Down Expand Up @@ -118,9 +138,9 @@ export const ConstraintCard = ({ application, environment, version, constraint,
</div>
)
}
title={shortSummary(constraint)}
title={getConstraintSummary(constraint)}
isActive={true}
noticeType={constraintCardAppearanceByStatus[status]}
noticeType={getCardAppearance(constraint)}
/>
);
};
Expand Up @@ -2,10 +2,11 @@ import React from 'react';
import { DateTime } from 'luxon';

import { relativeTime, timestamp } from '../../utils';
import { IStatefulConstraint, StatefulConstraintStatus } from '../../domain';
import { IStatefulConstraint, StatefulConstraintStatus, IStatelessConstraint } from '../../domain';
import { IconNames } from '../../presentation';

const NO_FAILURE_MESSAGE = 'no details available';
const UNKNOWN_CONSTRAINT_ICON = 'mdConstraintGeneric';

const throwUnhandledStatusError = (status: string) => {
throw new Error(`Unhandled constraint status "${status}", no constraint summary available`);
Expand All @@ -24,15 +25,53 @@ interface IStatefulConstraintConfig {
overrideActions: { [status in StatefulConstraintStatus]?: IConstraintOverrideAction[] };
}

export const isConstraintSupported = (type: string) => statefulConstraintOptionsByType.hasOwnProperty(type);
export const getStatefulConstraintConfig = (type: string) => statefulConstraintOptionsByType[type];
export const getStatefulConstraintActions = ({ type, status }: IStatefulConstraint) =>
statefulConstraintOptionsByType[type]?.overrideActions?.[status] ?? null;
interface IStatelessConstraintConfig {
iconName: IconNames;
shortSummary: {
pass: (constraint: IStatelessConstraint) => React.ReactNode;
fail: (constraint: IStatelessConstraint) => React.ReactNode;
};
}

export const isConstraintSupported = (type: string) =>
statefulConstraintOptionsByType.hasOwnProperty(type) || statelessConstraintOptionsByType.hasOwnProperty(type);

export const isConstraintStateful = (constraint: IStatefulConstraint | IStatelessConstraint) =>
statefulConstraintOptionsByType.hasOwnProperty(constraint.type);

export const getConstraintIcon = (type: string) =>
(statefulConstraintOptionsByType[type]?.iconName || statelessConstraintOptionsByType[type]?.iconName) ??
UNKNOWN_CONSTRAINT_ICON;

export const getConstraintActions = (constraint: IStatefulConstraint | IStatelessConstraint) => {
if (!isConstraintStateful(constraint)) {
return null;
}

const { type, status } = constraint as IStatefulConstraint;
return statefulConstraintOptionsByType[type]?.overrideActions?.[status] ?? null;
};

export const getConstraintSummary = (constraint: IStatefulConstraint | IStatelessConstraint) => {
if (isConstraintStateful(constraint)) {
return getStatefulConstraintSummary(constraint as IStatefulConstraint);
} else {
return getStatelessConstraintSummary(constraint as IStatelessConstraint);
}
};

const getStatefulConstraintSummary = (constraint: IStatefulConstraint) =>
statefulConstraintOptionsByType[constraint.type]?.shortSummary(constraint) ?? null;

const getStatelessConstraintSummary = (constraint: IStatelessConstraint) => {
const { pass, fail } = statelessConstraintOptionsByType[constraint.type]?.shortSummary ?? {};
return (constraint.currentlyPassing ? pass?.(constraint) : fail?.(constraint)) ?? null;
};

// Later, this will become a "proper" registry so we can separate configs
// into their own files and extend them dynamically at runtime.
// For now let's get more of the details settled and iterated on.
export const statefulConstraintOptionsByType: { [type: string]: IStatefulConstraintConfig } = {
const statefulConstraintOptionsByType: { [type: string]: IStatefulConstraintConfig } = {
'manual-judgement': {
iconName: 'manualJudgement',
shortSummary: ({ status, judgedAt, judgedBy, startedAt, comment }: IStatefulConstraint) => {
Expand Down Expand Up @@ -83,3 +122,15 @@ export const statefulConstraintOptionsByType: { [type: string]: IStatefulConstra
},
},
};

const statelessConstraintOptionsByType: { [type: string]: IStatelessConstraintConfig } = {
'depends-on': {
iconName: 'mdConstraintDependsOn',
shortSummary: {
pass: ({ attributes: { environment } }) =>
`Already deployed to previous environment ${environment?.toUpperCase()}`,
fail: ({ attributes: { environment } }) =>
`Deployment to ${environment?.toUpperCase()} will be required before promotion`,
},
},
};

0 comments on commit 0ee1888

Please sign in to comment.