Skip to content
Merged
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
133 changes: 20 additions & 113 deletions web/components/service/details/service-details-overview.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
"use client";

import cronstrue from "cronstrue";
import {
Activity,
Box,
Cpu,
Github,
Globe,
HardDrive,
type LucideIcon,
Server,
} from "lucide-react";
Expand Down Expand Up @@ -160,7 +157,7 @@ export function ServiceDetailsOverview({ service }: { service: Service }) {
});

return (
<Card className="gap-0 py-0">
<Card className="gap-0 border border-border py-0 ring-0">
<div className="grid items-stretch lg:grid-cols-[minmax(0,1.2fr)_minmax(360px,0.8fr)]">
<RequestStatsPanel
hasPublicHttp={overview.publicHttpCount > 0}
Expand All @@ -186,7 +183,7 @@ function RequestStatsPanel({
error?: unknown;
isLoading: boolean;
}) {
const [chartMode, setChartMode] = useState<RequestChartMode>("rate");
const [chartMode, setChartMode] = useState<RequestChartMode>("total");
const chartRows = useMemo(() => buildChartRows(stats), [stats]);
const statusSeries = useMemo(() => buildStatusSeries(stats), [stats]);
const hasChartData = chartRows.some((row) => row.totalRequests > 0);
Expand Down Expand Up @@ -358,12 +355,12 @@ function RequestChartModeToggle({
disabled: boolean;
}) {
const options: Array<{ value: RequestChartMode; label: string }> = [
{ value: "rate", label: "RPS" },
{ value: "total", label: "Total" },
{ value: "rate", label: "RPS" },
];

return (
<div className="inline-flex rounded-md border bg-muted/30 p-0.5">
<div className="inline-flex rounded-md border border-border bg-muted/30 p-0.5">
{options.map((option) => {
const isSelected = value === option.value;

Expand Down Expand Up @@ -401,7 +398,7 @@ function ServiceConfigPanel({
const primaryEndpoint = getPrimaryEndpoint(overview.endpoints);

return (
<div className="min-w-0 border-t p-4 lg:border-t-0 lg:border-l">
<div className="min-w-0 border-border border-t p-4 lg:border-t-0 lg:border-l">
<div className="grid gap-3 sm:grid-cols-2">
<SummaryItem icon={Activity} label="Status">
<StatusValue status={overview.status} />
Expand All @@ -411,6 +408,7 @@ function ServiceConfigPanel({
</SummaryItem>
<SummaryItem icon={Server} label="Instances">
<ServerList servers={overview.serverSummaries} />
<ConfigChip>{formatResources(service)}</ConfigChip>
</SummaryItem>

<ConfigDigestItem
Expand All @@ -432,40 +430,16 @@ function ServiceConfigPanel({
</ConfigChip>
</ConfigDigestItem>

<ConfigDigestItem
icon={Cpu}
label="Capacity"
primary={formatResources(service)}
>
<ConfigChip>{formatReplicaCompact(overview)}</ConfigChip>
<ConfigChip>{formatPlacementLabel(service)}</ConfigChip>
{overview.serverSummaries.length > 0 ? (
<ConfigChip>
{formatCount(overview.serverSummaries.length, "server")}
</ConfigChip>
) : null}
</ConfigDigestItem>

<ConfigDigestItem
icon={Globe}
label="Network"
primary={<EndpointPrimary endpoint={primaryEndpoint} />}
>
<ConfigChip>{formatEndpointCount(overview.endpoints)}</ConfigChip>
<ConfigChip>{formatPortSummary(service.ports || [])}</ConfigChip>
{primaryEndpoint.kind !== "private" ? (
<ConfigChip>{`${service.hostname || service.name}.internal`}</ConfigChip>
) : null}
</ConfigDigestItem>

<ConfigDigestItem
icon={HardDrive}
label="Data & ops"
primary={formatDataSummary(service)}
>
<ConfigChip>{formatBackupLabel(service)}</ConfigChip>
<ConfigChip>{formatDeployScheduleLabel(service)}</ConfigChip>
</ConfigDigestItem>
</div>
</div>
);
Expand All @@ -483,7 +457,12 @@ function SummaryItem({
className?: string;
}) {
return (
<div className={cn("min-w-0 rounded-md border bg-muted/20 p-4", className)}>
<div
className={cn(
"min-w-0 rounded-md border border-border bg-muted/20 p-4",
className,
)}
>
<div className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
<Icon className="size-4" />
<span>{label}</span>
Expand All @@ -509,7 +488,12 @@ function ConfigDigestItem({
className?: string;
}) {
return (
<div className={cn("min-w-0 rounded-md border bg-muted/20 p-4", className)}>
<div
className={cn(
"min-w-0 rounded-md border border-border bg-muted/20 p-4",
className,
)}
>
<div className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
<Icon className="size-4" />
<span>{label}</span>
Expand Down Expand Up @@ -630,7 +614,7 @@ function LegendMetric({

function RequestStatsState({ message }: { message: string }) {
return (
<div className="flex h-full items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
<div className="flex h-full items-center justify-center rounded-lg border border-border border-dashed text-sm text-muted-foreground">
{message}
</div>
);
Expand All @@ -649,7 +633,7 @@ function RequestStatsTooltip({
const items = visiblePayload.length > 0 ? visiblePayload : payload;

return (
<div className="rounded-lg border bg-popover px-3 py-2 text-xs shadow-md">
<div className="rounded-lg border border-border bg-popover px-3 py-2 text-xs shadow-md">
<p className="mb-1 font-medium">{formatTooltipDate(String(label))}</p>
<div className="space-y-1">
{items.map((item) => (
Expand Down Expand Up @@ -922,18 +906,6 @@ function formatInstanceSummary(overview: OverviewData): string {
return `${overview.runningDeployments}/${configured} Running Across ${formatCount(serverCount, "server")}`;
}

function getLockedServerLabel(service: Service): string | null {
if (!service.lockedServerId) return null;

return (
service.lockedServer?.name ??
service.configuredReplicas.find(
(replica) => replica.serverId === service.lockedServerId,
)?.serverName ??
service.lockedServerId
);
}

function getPrimaryEndpoint(endpoints: EndpointItem[]): EndpointItem {
return (
endpoints.find((endpoint) => endpoint.kind === "public") ??
Expand All @@ -948,34 +920,6 @@ function getPrimaryEndpoint(endpoints: EndpointItem[]): EndpointItem {
);
}

function formatReplicaCompact(overview: OverviewData): string {
const configured = getConfiguredReplicaCount(overview);

if (configured === 0) return "No Replicas";

return `${overview.runningDeployments}/${configured} Running`;
}

function formatPlacementLabel(service: Service): string {
const lockedServer = getLockedServerLabel(service);

if (service.stateful) {
return lockedServer ? `Stateful on ${lockedServer}` : "Stateful";
}

return lockedServer ? `Pinned to ${lockedServer}` : "Stateless";
}

function formatEndpointCount(endpoints: EndpointItem[]): string {
const publicCount = endpoints.filter(
(endpoint) => endpoint.kind !== "private",
).length;

if (publicCount === 0) return "Private Only";

return `${formatCount(publicCount, "public endpoint")} + Private`;
}

function formatPortSummary(ports: Service["ports"]): string {
if (ports.length === 0) return "No Ports";
if (ports.length === 1) return formatPortLabel(ports[0]);
Expand All @@ -990,43 +934,6 @@ function formatPortLabel(port: Service["ports"][number]): string {
return `${protocol} :${port.port}${external}`;
}

function formatDataSummary(service: Service): string {
const volumeCount = service.volumes?.length ?? 0;
const secretCount = service.secrets?.length ?? 0;
const parts = [];

if (volumeCount > 0) parts.push(formatCount(volumeCount, "volume"));
if (secretCount > 0) parts.push(formatCount(secretCount, "secret"));

return parts.length > 0 ? parts.join(" · ") : "No Volumes or Secrets";
}

function formatBackupLabel(service: Service): string {
if ((service.volumes?.length ?? 0) === 0) return "No Backups";
if (!service.backupEnabled) return "Manual Backups";

return service.backupSchedule
? `Backup ${service.backupSchedule}`
: "Backups On";
}

function formatDeployScheduleLabel(service: Service): string {
if (!service.deploymentSchedule) return "Manual Deploy";

try {
const scheduleDescription = cronstrue.toString(service.deploymentSchedule, {
verbose: true,
});
return `Deploy ${lowercaseFirstLetter(scheduleDescription)}`;
} catch {
return "Scheduled Deploy";
}
}

function lowercaseFirstLetter(value: string): string {
return value.charAt(0).toLowerCase() + value.slice(1);
}

function formatCount(count: number, singular: string): string {
const label = singular
.split(" ")
Expand Down
Loading