import { useSref } from '@uirouter/react'; import classnames from 'classnames'; import { sortBy } from 'lodash'; import type { DateTime } from 'luxon'; import React from 'react'; import type { IconNames } from '@spinnaker/presentation'; import { Icon } from '@spinnaker/presentation'; import { formatToRelativeTimestamp, RelativeTimestamp } from '../RelativeTimestamp'; import type { LifecycleEventSummary } from '../overview/artifact/utils'; import { HoverablePopover, IconTooltip, LabeledValue, Tooltip } from '../../presentation'; import { copyTextToClipboard } from '../../utils/clipboard/copyTextToClipboard'; import { ABSOLUTE_TIME_FORMAT, TOOLTIP_DELAY_SHOW } from '../utils/defaults'; import { useLogEvent } from '../utils/logging'; import './VersionMetadata.less'; export const MetadataElement: React.FC<{ className?: string }> = ({ className, children }) => { return {children}; }; export const METADATA_TEXT_COLOR = 'nobel'; export interface VersionMessageData { by?: string; at?: string; comment?: string; } export interface ICompareLinks { previous?: string; current?: string; } export const toPinnedMetadata = (data: { pinnedAt?: string; pinnedBy?: string; comment?: string; }): VersionMessageData => ({ by: data.pinnedBy, at: data.pinnedAt, comment: data.comment, }); export const toVetoedMetadata = (data: { vetoedAt?: string; vetoedBy?: string; comment?: string; }): VersionMessageData => ({ by: data.vetoedBy, at: data.vetoedAt, comment: data.comment, }); export interface IVersionMetadataProps { build?: IVersionBuildProps['build']; author?: string; deployedAt?: string; buildsBehind?: number; isDeploying?: boolean; isPending?: boolean; bake?: LifecycleEventSummary; pinned?: VersionMessageData; vetoed?: VersionMessageData; isCurrent?: boolean; } const useCreateVersionLink = (linkProps: IVersionCreatedAtProps['linkProps']) => { return useSref('home.applications.application.environments.history', linkProps); }; interface IVersionCreatedAtProps { createdAt?: string | DateTime; linkProps: Record; } export const VersionCreatedAt = ({ createdAt, linkProps }: IVersionCreatedAtProps) => { const { href } = useCreateVersionLink(linkProps); if (!createdAt) return null; return ( { href && copyTextToClipboard([window.location.origin, href].join('/')); e.stopPropagation(); }} > ); }; const badgeTypeToDetails = { deploying: { className: 'version-deploying', text: 'Deploying' }, baking: { className: 'version-baking', text: 'Baking' }, deployed: { className: 'version-deployed', text: 'Live' }, }; interface IMetadataBadgeProps { type: keyof typeof badgeTypeToDetails; tooltip?: string; link?: string; } export const MetadataBadge = ({ type, link, tooltip }: IMetadataBadgeProps) => { const details = badgeTypeToDetails[type]; const className = classnames('version-badge', details.className); const baseBadge = link ? ( {details.text} ) : ( {details.text} ); return ( {tooltip ? ( {baseBadge} ) : ( baseBadge )} ); }; interface IVersionMessage { data: VersionMessageData; type: 'pinned' | 'vetoed'; newRow?: boolean; } const versionTypeProps: { [key in IVersionMessage['type']]: { text: string; className: string; icon: IconNames } } = { pinned: { text: 'Pinned by', className: 'version-pinned', icon: 'pin', }, vetoed: { text: 'Rejected by', className: 'version-vetoed', icon: 'artifactBad', }, }; export const VersionMessage = ({ data, type, newRow = true }: IVersionMessage) => { const typeProps = versionTypeProps[type]; return ( <> {newRow &&
}
{typeProps.text} {data.by},{' '} {data.at && ( )}
{data.comment &&
Reason: {data.comment}
}
); }; interface IVersionAuthorProps { author?: string; } export const VersionAuthor = ({ author }: IVersionAuthorProps) => { if (!author) return null; return By {author}; }; interface IVersionBranchProps { branch?: string; } export const VersionBranch = ({ branch }: IVersionBranchProps) => { if (!branch) return null; return ( {branch} ); }; interface IVersionBuildProps { build: { buildNumber?: string; version?: string } & Partial; withPrefix?: boolean; } export const VersionBuild = ({ build, withPrefix }: IVersionBuildProps) => { const logEvent = useLogEvent('ArtifactBuild', 'OpenBuild'); const text = `${withPrefix ? `Build ` : ''}#${build.buildNumber}`; const content = build.link ? ( logEvent({ data: { build: build.buildNumber } })}> {text} ) : ( text ); return build.version ? ( } delayShow={TOOLTIP_DELAY_SHOW} > {content} ) : ( <>{content} ); }; interface IVersionBuildsProps { builds: Array; } export const VersionBuilds = ({ builds }: IVersionBuildsProps) => { if (!builds.length) return null; return ( {builds.length === 1 ? ( ) : ( <> Builds{' '} {sortBy(builds, (build) => build.buildNumber).map((build, index) => ( {Boolean(index < builds.length - 1) && ', '} ))} )} ); }; export const BaseVersionMetadata: React.FC = ({ children }) => { return
{children}
; }; interface ILifecycleEventDetailsProps extends Partial { title: string; showLink?: boolean; version?: string; } export const LifecycleEventDetails = ({ version, duration, link, startedAt, title, showLink = true, }: ILifecycleEventDetailsProps) => { return (
{title}
{version && } {showLink && link && Open} />}
); }; type IDeploymentStatusProps = Pick; const statusToProps = { CURRENT: { icon: 'cloudDeployed', tooltip: 'Deployed at', }, PENDING: { icon: 'cloudWaiting', tooltip: 'Deployment pending', }, PREVIOUS: { icon: 'cloudDecommissioned', tooltip: 'Replaced by another version', }, SKIPPED: { icon: 'artifactSkipped', tooltip: 'Deployment skipped', }, } as const; export const DeploymentStatus = ({ deployedAt, isCurrent, isPending, isDeploying }: IDeploymentStatusProps) => { if (!deployedAt && isDeploying) return null; // We'll show the deploying badge so no reason to show this component if this version is being deployed for the first time const props = statusToProps[deployedAt ? (isCurrent ? 'CURRENT' : 'PREVIOUS') : isPending ? 'PENDING' : 'SKIPPED']; return ( {deployedAt ? ( <> Deployed ) : isPending ? ( 'Pending' ) : ( 'Not deployed' )} ); };