From b46c648ffad29f8424101935841e65aa3cca2ed5 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Wed, 15 Jul 2026 17:50:33 +1000 Subject: [PATCH] refactor(web): polish service deployment views --- web/components/builds/build-details.tsx | 23 +- web/components/builds/builds-viewer.tsx | 31 +-- .../service/details/changelog-history.tsx | 69 ++--- .../service/details/rollout-details.tsx | 20 +- .../service/details/rollout-history.tsx | 33 ++- .../details/service-details-overview.tsx | 249 +++++++----------- web/components/ui/status-badge.tsx | 34 +++ 7 files changed, 207 insertions(+), 252 deletions(-) create mode 100644 web/components/ui/status-badge.tsx diff --git a/web/components/builds/build-details.tsx b/web/components/builds/build-details.tsx index 6b7aa5b..3430971 100644 --- a/web/components/builds/build-details.tsx +++ b/web/components/builds/build-details.tsx @@ -27,6 +27,7 @@ import { ItemDescription, ItemTitle, } from "@/components/ui/item"; +import { StatusBadge } from "@/components/ui/status-badge"; import type { Build, BuildStatus, GithubRepo, Service } from "@/db/types"; import { formatElapsedDurationBetween, formatRelativeTime } from "@/lib/date"; import { fetcher } from "@/lib/fetcher"; @@ -46,56 +47,47 @@ const STATUS_CONFIG: Record< { icon: typeof CheckCircle2; color: string; - bgColor: string; label: string; } > = { pending: { icon: Clock, color: "text-slate-500", - bgColor: "bg-slate-500/10", label: "Queued", }, claimed: { icon: CircleDashed, color: "text-blue-500", - bgColor: "bg-blue-500/10", label: "Starting", }, cloning: { icon: Loader2, color: "text-blue-500", - bgColor: "bg-blue-500/10", label: "Cloning", }, building: { icon: Loader2, color: "text-blue-500", - bgColor: "bg-blue-500/10", label: "Building", }, pushing: { icon: Loader2, color: "text-blue-500", - bgColor: "bg-blue-500/10", label: "Pushing", }, completed: { icon: CheckCircle2, color: "text-green-500", - bgColor: "bg-green-500/10", label: "Completed", }, failed: { icon: XCircle, color: "text-red-500", - bgColor: "bg-red-500/10", label: "Failed", }, cancelled: { icon: AlertCircle, color: "text-slate-500", - bgColor: "bg-slate-500/10", label: "Cancelled", }, }; @@ -128,7 +120,6 @@ export function BuildDetails({ const build = data?.build || initialBuild; const config = STATUS_CONFIG[build.status]; - const Icon = config.icon; const isAnimated = ["cloning", "building", "pushing"].includes(build.status); const handleCancel = async () => { @@ -188,12 +179,12 @@ export function BuildDetails({
- - - {config.label} - + {build.commitSha.slice(0, 7)} diff --git a/web/components/builds/builds-viewer.tsx b/web/components/builds/builds-viewer.tsx index c98fa1a..0ce7433 100644 --- a/web/components/builds/builds-viewer.tsx +++ b/web/components/builds/builds-viewer.tsx @@ -34,19 +34,14 @@ import { ItemTitle, } from "@/components/ui/item"; import { Spinner } from "@/components/ui/spinner"; +import { StatusBadge } from "@/components/ui/status-badge"; import type { Build, BuildStatus } from "@/db/types"; import { formatElapsedDurationBetween, formatRelativeTime } from "@/lib/date"; import { fetcher } from "@/lib/fetcher"; type BuildListItem = Pick< Build, - | "id" - | "commitSha" - | "commitMessage" - | "branch" - | "author" - | "status" - | "error" + "id" | "commitSha" | "commitMessage" | "branch" | "author" | "status" > & { createdAt: string; startedAt: string | null; @@ -103,18 +98,17 @@ const STATUS_CONFIG: Record< }, }; -function StatusBadge({ status }: { status: BuildStatus }) { +function BuildStatusBadge({ status }: { status: BuildStatus }) { const config = STATUS_CONFIG[status]; - const Icon = config.icon; const isAnimated = ["cloning", "building", "pushing"].includes(status); return ( - - - {config.label} - + ); } @@ -243,7 +237,7 @@ export function BuildsViewer({ ) } > - + @@ -275,11 +269,6 @@ export function BuildsViewer({ )} - {build.error && ( -
-										{build.error}
-									
- )}
e.stopPropagation()}> {canCancel(build.status) && ( diff --git a/web/components/service/details/changelog-history.tsx b/web/components/service/details/changelog-history.tsx index 364db40..c2bcddc 100644 --- a/web/components/service/details/changelog-history.tsx +++ b/web/components/service/details/changelog-history.tsx @@ -1,7 +1,6 @@ "use client"; import { - ArrowRight, CheckCircle2, Clock, GitCommitHorizontal, @@ -12,7 +11,6 @@ import { import Link from "next/link"; import { useMemo } from "react"; import useSWRInfinite from "swr/infinite"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Empty, @@ -21,6 +19,7 @@ import { EmptyTitle, } from "@/components/ui/empty"; import { Skeleton } from "@/components/ui/skeleton"; +import { StatusBadge } from "@/components/ui/status-badge"; import type { RolloutStatus } from "@/db/types"; import { formatDateTime, formatRelativeTime } from "@/lib/date"; import { fetcher } from "@/lib/fetcher"; @@ -68,23 +67,19 @@ function RolloutBadge({ icon: Clock, className: "text-muted-foreground", }; - const Icon = config.icon; return ( - } - > - - {config.label} - + /> ); } @@ -127,18 +122,25 @@ function RevisionChanges({ item }: { item: ServiceRevisionChangelogItem }) { } return ( -
+
{item.comparison.changes.map((change) => (
-
{change.field}
-
- {change.from} - - {change.to} -
+ {change.field} + + + {change.from} + + โ†’ + + {change.to} + +
))}
@@ -219,29 +221,30 @@ export function ChangelogHistory({ ) : ( -
- {revisions.map((revision) => ( -
-
-
-
+
+ {revisions.map((revision, index) => ( +
+ {index < revisions.length - 1 ? ( + + ) : null} +
+ +
+ {revision.comparison.kind === "initial" ? "Initial revision" : revision.comparison.kind === "changes" && revision.comparison.changes.length === 0 ? "Redeployed" : "Configuration updated"} -
-
+ {formatRelativeTime(revision.createdAt)} ยท{" "} {revision.id.slice(0, 8)} -
+
{revision.rollout ? ( ) : null}
- +
+ +
))}
diff --git a/web/components/service/details/rollout-details.tsx b/web/components/service/details/rollout-details.tsx index d6adce6..473ecd1 100644 --- a/web/components/service/details/rollout-details.tsx +++ b/web/components/service/details/rollout-details.tsx @@ -11,6 +11,7 @@ import { import { useRouter } from "next/navigation"; import { LogViewer } from "@/components/logs/log-viewer"; import { Button } from "@/components/ui/button"; +import { StatusBadge } from "@/components/ui/status-badge"; import type { Rollout, RolloutStatus, Service } from "@/db/types"; import { formatElapsedDurationBetween, formatRelativeTime } from "@/lib/date"; @@ -24,38 +25,32 @@ const STATUS_CONFIG: Record< { icon: typeof CheckCircle2; color: string; - bgColor: string; label: string; } > = { queued: { icon: Clock, color: "text-slate-500", - bgColor: "bg-slate-500/10", label: "Queued", }, in_progress: { icon: Loader2, color: "text-blue-500", - bgColor: "bg-blue-500/10", label: "In Progress", }, completed: { icon: CheckCircle2, color: "text-green-500", - bgColor: "bg-green-500/10", label: "Completed", }, failed: { icon: XCircle, color: "text-red-500", - bgColor: "bg-red-500/10", label: "Failed", }, rolled_back: { icon: RotateCcw, color: "text-orange-500", - bgColor: "bg-orange-500/10", label: "Rolled Back", }, }; @@ -88,7 +83,6 @@ export function RolloutDetails({ }) { const router = useRouter(); const config = STATUS_CONFIG[rollout.status as RolloutStatus]; - const Icon = config.icon; const isLive = rollout.status === "queued" || rollout.status === "in_progress"; const isAnimated = rollout.status === "in_progress"; @@ -109,12 +103,12 @@ export function RolloutDetails({
- - - {config.label} - + {rollout.currentStage && isLive && ( {formatStage(rollout.currentStage)} diff --git a/web/components/service/details/rollout-history.tsx b/web/components/service/details/rollout-history.tsx index 74dd12c..5665b9b 100644 --- a/web/components/service/details/rollout-history.tsx +++ b/web/components/service/details/rollout-history.tsx @@ -18,6 +18,7 @@ import { ItemTitle, } from "@/components/ui/item"; import { Skeleton } from "@/components/ui/skeleton"; +import { StatusBadge } from "@/components/ui/status-badge"; import type { RolloutStatus } from "@/db/types"; import { formatElapsedDurationBetween, formatRelativeTime } from "@/lib/date"; import { fetcher } from "@/lib/fetcher"; @@ -66,18 +67,24 @@ const STATUS_CONFIG: Record< }, }; -function StatusBadge({ status }: { status: RolloutStatus }) { +const STATUS_TITLES: Record, string> = { + queued: "Waiting to begin deployment", + completed: "Deployment completed successfully", + failed: "Deployment failed", + rolled_back: "Deployment rolled back", +}; + +function RolloutStatusBadge({ status }: { status: RolloutStatus }) { const config = STATUS_CONFIG[status]; - const Icon = config.icon; const isAnimated = status === "in_progress"; return ( - - - {config.label} - + ); } @@ -182,15 +189,13 @@ export function RolloutHistory({ href={`/dashboard/projects/${projectSlug}/${envName}/services/${serviceId}/rollouts/${rollout.id}`} > - + - {rollout.status === "queued" - ? "Queued" - : rollout.status === "in_progress" - ? `Deploying โ€” ${formatStage(rollout.currentStage)}` - : STATUS_CONFIG[rollout.status].label} + {rollout.status === "in_progress" + ? `Deploying โ€” ${formatStage(rollout.currentStage)}` + : STATUS_TITLES[rollout.status]} diff --git a/web/components/service/details/service-details-overview.tsx b/web/components/service/details/service-details-overview.tsx index a5d584b..5bc2672 100644 --- a/web/components/service/details/service-details-overview.tsx +++ b/web/components/service/details/service-details-overview.tsx @@ -1,13 +1,6 @@ "use client"; -import { - Activity, - Box, - Github, - Globe, - type LucideIcon, - Server, -} from "lucide-react"; +import { Box, Github, type LucideIcon } from "lucide-react"; import { type ReactNode, useMemo, useState } from "react"; import { CartesianGrid, @@ -20,7 +13,6 @@ import { } from "recharts"; import useSWR from "swr"; import { useService } from "@/components/service/service-layout-client"; -import { Badge } from "@/components/ui/badge"; import { Card } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import type { ServiceWithDetails as Service } from "@/db/types"; @@ -442,148 +434,129 @@ function ServiceConfigPanel({ overview: OverviewData; }) { const primaryEndpoint = getPrimaryEndpoint(overview.endpoints); + const statusClasses = STATUS_TONE_CLASSES[overview.status.tone]; + const hasResourceLimits = + service.resourceCpuLimit != null || service.resourceMemoryLimitMb != null; return ( -
-
- - -

- {formatInstanceSummary(overview)} -

-
- - - {formatResources(service)} - - - } - > - - {overview.source.branch || overview.source.detail} - +
+
+
+ + + {overview.status.label} + +
+ + {formatInstanceSummary(overview)} + +
+ +
+
+ + + + {overview.source.branch ? ( + {overview.source.branch} + ) : null} {service.githubRootDir ? ( - {service.githubRootDir} + + {service.githubRootDir} + ) : null} - - {service.startCommand ? "Custom Command" : "Image Command"} - - - {service.healthCheckCmd ? "Health Check" : "No Health Check"} - - - - } - > - {formatPortSummary(service.ports || [])} + + {service.startCommand ? "Custom" : "Image default"} + + + {service.healthCheckCmd ? "Configured" : "None"} + + + {hasResourceLimits ? formatResources(service) : "Not set"} + +
+ +
+ {overview.serverSummaries.length === 0 ? ( +

No servers configured

+ ) : ( + overview.serverSummaries.map((server) => ( + + + {server.configured > 0 + ? `${server.running}/${server.configured} running` + : `${server.running} running`} + + + )) + )} +
+ +
+ + + + + {formatPortSummary(service.ports || [])} + {primaryEndpoint.kind !== "private" ? ( - {`${service.hostname || service.name}.internal`} + + {`${service.hostname || service.name}.internal`} + ) : null} - +
); } -function SummaryItem({ - icon: Icon, +function ConfigRow({ label, children, - className, + muted = false, }: { - icon: LucideIcon; label: string; children: ReactNode; - className?: string; + muted?: boolean; }) { return ( -
-
- - {label} -
-
+
+ {label} + {children} -
-
- ); -} - -function ConfigDigestItem({ - icon: Icon, - label, - primary, - children, - className, -}: { - icon: LucideIcon; - label: string; - primary: ReactNode; - children: ReactNode; - className?: string; -}) { - return ( -
-
- - {label} -
-
- {primary} -
-
{children}
+
); } -function ConfigChip({ - children, - tone = "muted", -}: { - children: ReactNode; - tone?: "active" | "muted"; -}) { - return ( - - {children} - +function SourcePrimary({ source }: { source: SourceInfo }) { + const Icon = source.icon; + const content = ( + <> + + {source.label} + ); -} -function SourcePrimary({ source }: { source: SourceInfo }) { if (source.href) { return ( - {source.label} + {content} ); } - return {source.label}; + return content; } function EndpointPrimary({ endpoint }: { endpoint: EndpointItem }) { @@ -593,48 +566,14 @@ function EndpointPrimary({ endpoint }: { endpoint: EndpointItem }) { href={endpoint.href} target="_blank" rel="noopener noreferrer" - className="block min-w-0 truncate hover:text-primary" + className="hover:text-primary" > {endpoint.label} ); } - return {endpoint.label}; -} - -function StatusValue({ status }: { status: ServiceStatus }) { - const classes = STATUS_TONE_CLASSES[status.tone]; - - return ( -
- - - {status.label} - -
- ); -} - -function ServerList({ servers }: { servers: ServerSummary[] }) { - if (servers.length === 0) { - return

No servers configured

; - } - - return ( -
- {servers.map((server) => ( - - {server.name} - - {server.configured > 0 - ? `${server.running}/${server.configured}` - : `${server.running} Running`} - - - ))} -
- ); + return <>{endpoint.label}; } function LegendMetric({ @@ -949,15 +888,13 @@ function formatInstanceSummary(overview: OverviewData): string { (total, server) => total + server.configured, 0, ); - const serverCount = overview.serverSummaries.length; - if (configured === 0) { return overview.runningDeployments > 0 - ? `${overview.runningDeployments} Running` - : "No Replicas"; + ? `${overview.runningDeployments} running` + : "No replicas"; } - return `${overview.runningDeployments}/${configured} Running Across ${formatCount(serverCount, "server")}`; + return `${overview.runningDeployments}/${configured} running`; } function getPrimaryEndpoint(endpoints: EndpointItem[]): EndpointItem { diff --git a/web/components/ui/status-badge.tsx b/web/components/ui/status-badge.tsx new file mode 100644 index 0000000..1f9c3dd --- /dev/null +++ b/web/components/ui/status-badge.tsx @@ -0,0 +1,34 @@ +import type * as React from "react"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +type StatusBadgeProps = Omit, "children"> & { + icon: React.ComponentType<{ className?: string }>; + label: string; + isAnimated?: boolean; +}; + +function StatusBadge({ + icon: Icon, + label, + isAnimated = false, + className, + variant = "outline", + ...props +}: StatusBadgeProps) { + return ( + svg]:size-3.5!", + className, + )} + {...props} + > + + {label} + + ); +} + +export { StatusBadge };