Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export default function DeploymentsPage() {

return (
<div>
<ServiceDetailsOverview service={service} />

<DeploymentProgress
service={service}
changes={pendingChanges}
Expand All @@ -143,8 +145,6 @@ export default function DeploymentsPage() {
barMode={barState.mode}
/>

<ServiceDetailsOverview service={service} />

<div className="mt-4">
<RolloutHistory
serviceId={service.id}
Expand Down
107 changes: 61 additions & 46 deletions web/components/service/details/deployment-progress.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { Loader2, XCircle } from "lucide-react";
import { useRouter } from "next/navigation";
import { memo, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
Expand All @@ -13,6 +12,7 @@ import type {
ServiceWithDetails as Service,
} from "@/db/types";
import type { ConfigChange } from "@/lib/service-config";
import { cn } from "@/lib/utils";

type StageInfo = {
id: string;
Expand Down Expand Up @@ -168,6 +168,19 @@ export function getBarState(
return { mode: "hidden" };
}

function StageProgress({ current, total }: { current: number; total: number }) {
const progress = total > 0 ? Math.min((current + 1) / total, 1) : 0;

return (
<div className="h-0.5 w-full bg-border/60">
<div
className="h-full bg-blue-500 transition-[width] duration-500 ease-out"
style={{ width: `${progress * 100}%` }}
/>
</div>
);
}

interface DeploymentProgressProps {
service: Service;
changes: ConfigChange[];
Expand Down Expand Up @@ -253,19 +266,17 @@ export const DeploymentProgress = memo(function DeploymentProgress({
let content: React.ReactNode = null;

if (barState.mode === "building") {
const buildStageIndex = ACTIVE_BUILD_STATUSES.indexOf(barState.buildStatus);

content = (
<div className="rounded-lg border bg-card p-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-md bg-blue-500/10 text-blue-600 dark:text-blue-400">
<Loader2 className="size-4 animate-spin" />
</div>
<div>
<p className="font-medium text-foreground">Building</p>
<p className="text-sm text-muted-foreground">
{BUILD_STATUS_LABELS[barState.buildStatus] || "Building"}
</p>
</div>
<div className="mx-auto max-w-5xl overflow-hidden rounded-b-lg border border-blue-500/40 border-t-0 bg-blue-500/5">
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 px-3 py-2">
<div className="flex min-w-0 items-center gap-2 text-sm">
<span className="size-2 animate-pulse rounded-full bg-blue-500" />
<span className="font-medium">Building</span>
<span className="truncate text-muted-foreground">
{BUILD_STATUS_LABELS[barState.buildStatus] || "Building"}
</span>
</div>
<Button
variant="outline"
Expand All @@ -279,45 +290,48 @@ export const DeploymentProgress = memo(function DeploymentProgress({
View Logs
</Button>
</div>
<StageProgress
current={buildStageIndex}
total={ACTIVE_BUILD_STATUSES.length}
/>
</div>
);
}

if (barState.mode === "deploying") {
const currentStage = STAGES[barState.stageIndex];
const isMigrating = !!service.migrationStatus;
const isMigrationFailed = service.migrationStatus === "failed";

let status = currentStage?.label || "Deploying";
if (barState.stage === "health_check" && !service.healthCheckCmd) {
status = "Starting container";
}
if (isMigrating && service.migrationStatus) {
status =
MIGRATION_STAGES[service.migrationStatus] ||
service.migrationStatus ||
"Migrating";
}
const migrationStatus =
isMigrating && service.migrationStatus
? MIGRATION_STAGES[service.migrationStatus] || service.migrationStatus
: null;

content = (
<div className="rounded-lg border bg-card p-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
{isMigrationFailed ? (
<div className="p-2 rounded-md bg-rose-500/10 text-rose-600 dark:text-rose-400">
<XCircle className="size-4" />
</div>
) : (
<div className="p-2 rounded-md bg-blue-500/10 text-blue-600 dark:text-blue-400">
<Loader2 className="size-4 animate-spin" />
</div>
)}
<div>
<p className="font-medium text-foreground">
{isMigrating ? "Migrating" : "Deploying"}
</p>
<p className="text-sm text-muted-foreground">{status}</p>
</div>
<div
className={cn(
"mx-auto max-w-5xl overflow-hidden rounded-b-lg border border-t-0",
isMigrationFailed
? "border-red-500/40 bg-red-500/5"
: "border-blue-500/40 bg-blue-500/5",
)}
>
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 px-3 py-2">
<div className="flex min-w-0 items-center gap-2 text-sm">
<span
className={cn(
"size-2 rounded-full",
isMigrationFailed ? "bg-red-500" : "animate-pulse bg-blue-500",
)}
/>
<span className="font-medium">
{isMigrating ? "Migrating" : "Deploying"}
</span>
{migrationStatus ? (
<span className="truncate text-muted-foreground">
{migrationStatus}
</span>
) : null}
</div>
{isMigrating ? (
<Button
Expand All @@ -330,7 +344,7 @@ export const DeploymentProgress = memo(function DeploymentProgress({
</Button>
) : (
<Button
variant="outline"
variant="destructive"
size="sm"
onClick={handleAbort}
disabled={isAborting}
Expand All @@ -339,6 +353,9 @@ export const DeploymentProgress = memo(function DeploymentProgress({
</Button>
)}
</div>
{!isMigrating ? (
<StageProgress current={barState.stageIndex} total={STAGES.length} />
) : null}
</div>
);
}
Expand All @@ -351,9 +368,7 @@ export const DeploymentProgress = memo(function DeploymentProgress({
opacity: isVisible ? 1 : 0,
}}
>
<div className="overflow-hidden">
{content && <div className="pb-4">{content}</div>}
</div>
<div className="overflow-hidden">{content}</div>
</div>
);
});
105 changes: 52 additions & 53 deletions web/components/service/details/pending-changes-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { AlertTriangle, ArrowRight, Rocket } from "lucide-react";
import { Rocket } from "lucide-react";
import { useRouter } from "next/navigation";
import { memo, useState } from "react";
import { useSWRConfig } from "swr";
Expand Down Expand Up @@ -72,60 +72,59 @@ export const PendingChangesBanner = memo(function PendingChangesBanner({
}}
>
<div className="overflow-hidden">
<div className="pb-4">
<div className="rounded-lg border bg-card p-4">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 min-w-0">
<div className="p-2 rounded-md bg-amber-500/10 text-amber-600 dark:text-amber-400 shrink-0">
<AlertTriangle className="size-4" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground">
{hasChanges
? `${changes.length} pending change${changes.length !== 1 ? "s" : ""}`
: "Ready to deploy"}
</p>
{hasChanges ? (
<div className="mt-2 space-y-1.5">
{changes.map((change, index) => (
<div
key={`${change.field}:${change.from}:${change.to}:${index}`}
className="flex items-center gap-2 text-sm"
>
<span className="font-medium shrink-0 text-muted-foreground">
{change.field}:
</span>
<span className="text-muted-foreground truncate">
{change.from}
</span>
<ArrowRight className="size-3 shrink-0 text-muted-foreground" />
<span className="text-foreground truncate">
{change.to}
</span>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground mt-1">
This service has no active deployments.
</p>
)}
</div>
</div>
<Button
size="sm"
onClick={handleDeploy}
disabled={isDeploying || totalReplicas === 0}
>
{isDeploying ? (
<Spinner className="size-4" />
) : (
<Rocket className="size-4" data-icon="inline-start" />
)}
{isGithubWithNoDeployments ? "Build" : "Deploy"}
</Button>
<div className="mx-auto max-w-5xl rounded-b-lg border border-amber-500/40 border-t-0 bg-amber-500/5">
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 px-3 py-2">
<div className="flex items-center gap-2">
<span className="size-2 rounded-full bg-amber-500" />
<span className="text-sm font-medium">
{hasChanges
? `${changes.length} pending change${changes.length !== 1 ? "s" : ""}`
: "Ready to deploy"}
</span>
</div>
<Button
size="sm"
onClick={handleDeploy}
disabled={isDeploying || totalReplicas === 0}
>
{isDeploying ? (
<Spinner className="size-4" />
) : (
<Rocket className="size-4" data-icon="inline-start" />
)}
{isGithubWithNoDeployments ? "Build" : "Deploy"}
</Button>
</div>
{hasChanges ? (
<div className="space-y-1.5 border-amber-500/20 border-t px-3 py-2.5 font-mono text-sm">
{changes.map((change, index) => (
<div
key={`${change.field}:${change.from}:${change.to}:${index}`}
className="flex items-baseline justify-between gap-4"
>
<span className="shrink-0 text-muted-foreground">
{change.field}
</span>
<span className="flex min-w-0 items-baseline justify-end gap-1.5">
<span
className="truncate text-muted-foreground"
title={change.from}
>
{change.from}
</span>
<span className="shrink-0 text-muted-foreground">→</span>
<span className="truncate font-medium" title={change.to}>
{change.to}
</span>
</span>
</div>
))}
</div>
) : (
<p className="border-amber-500/20 border-t px-3 py-2.5 text-sm text-muted-foreground">
This service has no active deployments.
</p>
)}
</div>
</div>
</div>
Expand Down
Loading
Loading