diff --git a/app/scripts/modules/core/src/domain/IManagedEntity.ts b/app/scripts/modules/core/src/domain/IManagedEntity.ts index 06f6de57b22..d31ae0bebc4 100644 --- a/app/scripts/modules/core/src/domain/IManagedEntity.ts +++ b/app/scripts/modules/core/src/domain/IManagedEntity.ts @@ -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; @@ -72,7 +81,8 @@ export interface IManagedArtifactVersion { deployedAt?: string; replacedAt?: string; replacedBy?: string; - statefulConstraints: IStatefulConstraint[]; + statefulConstraints?: IStatefulConstraint[]; + statelessConstraints?: IStatelessConstraint[]; }>; } diff --git a/app/scripts/modules/core/src/managed/ArtifactDetail.tsx b/app/scripts/modules/core/src/managed/ArtifactDetail.tsx index 71ed241993e..1c4c5726b6f 100644 --- a/app/scripts/modules/core/src/managed/ArtifactDetail.tsx +++ b/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'; @@ -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'; @@ -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 & { constraints: Array }) => ( + <> + {constraints + .filter(({ type }) => isConstraintSupported(type)) + .map(constraint => ( + + ))} + + ), +); + export interface IArtifactDetailProps { application: Application; name: string; @@ -57,7 +81,15 @@ export const ArtifactDetail = ({
{/* artifact metadata will live here */}
{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 } = @@ -69,22 +101,9 @@ export const ArtifactDetail = ({ name={environmentName} resources={resourcesByEnvironment[environmentName]} > - {statefulConstraints && - statefulConstraints - .filter(({ type }) => isConstraintSupported(type)) - .map(constraint => ( - - ))} {state === 'deploying' && ( )} - {resourcesByEnvironment[environmentName] - .filter(resource => shouldDisplayResource(name, type, resource)) - .map(resource => ( -
- {state === 'deploying' && ( -
+ )} + {statelessConstraints && ( + + )} +
+ {resourcesByEnvironment[environmentName] + .filter(resource => shouldDisplayResource(name, type, resource)) + .map(resource => ( +
+ {state === 'deploying' && ( +
+ )} + - )} - -
- ))} +
+ ))} +
); }, diff --git a/app/scripts/modules/core/src/managed/constraints/ConstraintCard.tsx b/app/scripts/modules/core/src/managed/constraints/ConstraintCard.tsx index 8d112bdbfac..df3a7a96ee9 100644 --- a/app/scripts/modules/core/src/managed/constraints/ConstraintCard.tsx +++ b/app/scripts/modules/core/src/managed/constraints/ConstraintCard.tsx @@ -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'; @@ -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('NONE'); @@ -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 ( ) } - title={shortSummary(constraint)} + title={getConstraintSummary(constraint)} isActive={true} - noticeType={constraintCardAppearanceByStatus[status]} + noticeType={getCardAppearance(constraint)} /> ); }; diff --git a/app/scripts/modules/core/src/managed/constraints/constraintRegistry.tsx b/app/scripts/modules/core/src/managed/constraints/constraintRegistry.tsx index 3ebbe4f63c3..66794cf0cb2 100644 --- a/app/scripts/modules/core/src/managed/constraints/constraintRegistry.tsx +++ b/app/scripts/modules/core/src/managed/constraints/constraintRegistry.tsx @@ -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`); @@ -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) => { @@ -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`, + }, + }, +}; diff --git a/app/scripts/modules/core/src/presentation/icons/iconsByName.ts b/app/scripts/modules/core/src/presentation/icons/iconsByName.ts index 48a4c1770a2..69c141c467e 100644 --- a/app/scripts/modules/core/src/presentation/icons/iconsByName.ts +++ b/app/scripts/modules/core/src/presentation/icons/iconsByName.ts @@ -65,6 +65,9 @@ import { ReactComponent as mdFlapping } from './vectors/mdFlapping.svg'; import { ReactComponent as mdPaused } from './vectors/mdPaused.svg'; import { ReactComponent as mdResumed } from './vectors/mdResumed.svg'; import { ReactComponent as mdUnknown } from './vectors/mdUnknown.svg'; +import { ReactComponent as mdConstraintGeneric } from './vectors/mdConstraintGeneric.svg'; +import { ReactComponent as mdConstraintDependsOn } from './vectors/mdConstraintDependsOn.svg'; +import { ReactComponent as mdConstraintAllowedTimes } from './vectors/mdConstraintAllowedTimes.svg'; import { ReactComponent as md } from './vectors/md.svg'; export const iconsByName = { @@ -107,6 +110,9 @@ export const iconsByName = { mdPaused, mdResumed, mdUnknown, + mdConstraintGeneric, + mdConstraintDependsOn, + mdConstraintAllowedTimes, md, placeholder, securityGroup, diff --git a/app/scripts/modules/core/src/presentation/icons/vectors/mdConstraintAllowedTimes.svg b/app/scripts/modules/core/src/presentation/icons/vectors/mdConstraintAllowedTimes.svg new file mode 100644 index 00000000000..350cf1847d5 --- /dev/null +++ b/app/scripts/modules/core/src/presentation/icons/vectors/mdConstraintAllowedTimes.svg @@ -0,0 +1,26 @@ + + + + + time-clock-three + + + + + + + + + diff --git a/app/scripts/modules/core/src/presentation/icons/vectors/mdConstraintDependsOn.svg b/app/scripts/modules/core/src/presentation/icons/vectors/mdConstraintDependsOn.svg new file mode 100644 index 00000000000..1966a9279a9 --- /dev/null +++ b/app/scripts/modules/core/src/presentation/icons/vectors/mdConstraintDependsOn.svg @@ -0,0 +1,25 @@ + + + + + science-molecule-strucutre + + + + + diff --git a/app/scripts/modules/core/src/presentation/icons/vectors/mdConstraintGeneric.svg b/app/scripts/modules/core/src/presentation/icons/vectors/mdConstraintGeneric.svg new file mode 100644 index 00000000000..167c0992609 --- /dev/null +++ b/app/scripts/modules/core/src/presentation/icons/vectors/mdConstraintGeneric.svg @@ -0,0 +1,13 @@ + + + + + construction-cone + + +