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 };