Skip to content
Merged
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
28 changes: 24 additions & 4 deletions web/app/(dashboard)/layout-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ import {
import { Toaster } from "@/components/ui/sonner";
import { signOut, useSession } from "@/lib/auth-client";

function DashboardHeader({ email }: { email: string }) {
function DashboardHeader({
email,
name,
}: {
email: string;
name: string;
}) {
const router = useRouter();
const breadcrumbs = useBreadcrumbs();
const getBreadcrumbKey = (
Expand Down Expand Up @@ -113,9 +119,20 @@ function DashboardHeader({ email }: { email: string }) {
>
<User className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8}>
<DropdownMenuContent
align="end"
sideOffset={8}
className="w-40"
>
<DropdownMenuGroup>
<DropdownMenuLabel>{email}</DropdownMenuLabel>
<DropdownMenuLabel>
<span className="block truncate text-sm font-semibold">
{name}
</span>
<span className="block truncate text-xs font-normal text-muted-foreground">
{email}
</span>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
Expand Down Expand Up @@ -166,7 +183,10 @@ export function DashboardLayoutClient({
return (
<BreadcrumbDataProvider>
<div className="min-h-screen">
<DashboardHeader email={session.user.email} />
<DashboardHeader
email={session.user.email}
name={session.user.name}
/>
<OfflineServersBanner />
<main>{children}</main>
<Toaster />
Expand Down
40 changes: 26 additions & 14 deletions web/components/metrics/metrics-history-charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
NativeSelect,
NativeSelectOption,
} from "@/components/ui/native-select";
import { Spinner } from "@/components/ui/spinner";
import { Skeleton } from "@/components/ui/skeleton";
import { fetcher } from "@/lib/fetcher";
import { METRIC_RANGE_KEYS, type MetricRange } from "@/lib/metric-ranges";
import type {
Expand Down Expand Up @@ -178,12 +178,7 @@ export function MetricsHistoryCharts({
description="The metrics API could not be reached. Current health data may still be available."
/>
) : isLoading ? (
<MetricsStateCard
icon={Activity}
title="Loading metrics"
description="Fetching recent infrastructure history."
loading
/>
<MetricsChartsSkeleton />
) : !hasData ? (
<MetricsStateCard
icon={Activity}
Expand Down Expand Up @@ -330,26 +325,43 @@ function MetricChartCard({
);
}

function MetricsChartsSkeleton() {
return (
<div aria-hidden="true" className="grid gap-4">
{CHARTS.map((chart) => (
<Card key={chart.title}>
<CardHeader className="border-b">
<div className="flex items-center gap-3">
<Skeleton className="size-8 rounded-lg" />
<div className="space-y-2">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-4 w-56 max-w-full" />
</div>
</div>
</CardHeader>
<CardContent className="pt-4">
<Skeleton className="h-72 rounded-lg lg:h-80" />
</CardContent>
</Card>
))}
</div>
);
}

function MetricsStateCard({
icon: Icon,
title,
description,
loading = false,
}: {
icon: typeof Activity;
title: string;
description: string;
loading?: boolean;
}) {
return (
<Card>
<CardContent className="flex items-center gap-3 py-6">
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
{loading ? (
<Spinner className="size-4" />
) : (
<Icon className="size-4" />
)}
<Icon className="size-4" />
</div>
<div>
<p className="font-medium">{title}</p>
Expand Down
90 changes: 57 additions & 33 deletions web/components/server/server-services.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import Link from "next/link";
import { Box, Layers } from "lucide-react";
import { ArrowUpRight, Layers } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Item,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
import { getServerServices } from "@/db/queries";

export async function ServerServices({ serverId }: { serverId: string }) {
Expand All @@ -24,6 +16,8 @@ export async function ServerServices({ serverId }: { serverId: string }) {
return null;
}

const groups = groupServicesByProjectEnvironment(services);

return (
<Card>
<CardHeader>
Expand All @@ -37,32 +31,62 @@ export async function ServerServices({ serverId }: { serverId: string }) {
</CardDescription>
</CardHeader>
<CardContent>
<ItemGroup className="grid gap-3 md:grid-cols-2">
{services.map((service) => (
<Item
key={service.serviceId}
variant="outline"
render={
<Link
href={`/dashboard/projects/${service.projectSlug}/${service.environmentName}?service=${service.serviceId}`}
/>
}
>
<ItemMedia variant="icon">
<Box className="size-5 text-muted-foreground" />
</ItemMedia>
<ItemContent>
<ItemTitle className="truncate">
{service.serviceName}
</ItemTitle>
<ItemDescription className="truncate">
{service.projectName} / {service.environmentName}
</ItemDescription>
</ItemContent>
</Item>
<div className="space-y-5">
{groups.map((group) => (
<div key={group.key} className="space-y-2">
<div className="flex items-center gap-2">
<p className="font-medium text-sm">{group.projectName}</p>
<span className="text-xs text-muted-foreground">
{group.environmentName}
</span>
</div>
<div className="divide-y rounded-lg border">
{group.services.map((service) => (
<Link
key={service.serviceId}
href={`/dashboard/projects/${service.projectSlug}/${service.environmentName}/services/${service.serviceId}`}
className="flex items-center justify-between gap-3 px-3 py-2.5 text-sm transition-colors hover:bg-muted"
>
<span className="min-w-0 truncate font-medium">
{service.serviceName}
</span>
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
</Link>
))}
</div>
</div>
))}
</ItemGroup>
</div>
</CardContent>
</Card>
);
}

type ServerService = Awaited<ReturnType<typeof getServerServices>>[number];

function groupServicesByProjectEnvironment(services: ServerService[]) {
const groups = new Map<
string,
{
key: string;
projectName: string;
environmentName: string;
services: ServerService[];
}
>();

for (const service of services) {
const key = `${service.projectId}:${service.environmentName}`;
const group = groups.get(key) ?? {
key,
projectName: service.projectName,
environmentName: service.environmentName,
services: [],
};

group.services.push(service);
groups.set(key, group);
}

return Array.from(groups.values());
}
26 changes: 14 additions & 12 deletions web/components/settings/member-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,18 +244,20 @@ export function MemberSettings({ initialMembers, initialInvitations }: Props) {
<NativeSelectOption value="reader">Reader</NativeSelectOption>
</NativeSelect>
)}
<div className="flex justify-end">
<Button
type="button"
variant="ghost"
size="icon"
disabled={member.role === "admin" || busyId === member.id}
onClick={() => void handleRemove(member)}
title="Remove member"
>
<Trash2 className="size-4" />
</Button>
</div>
{member.role !== "admin" && (
<div className="flex justify-end">
<Button
type="button"
variant="ghost"
size="icon"
disabled={busyId === member.id}
onClick={() => void handleRemove(member)}
title="Remove member"
>
<Trash2 className="size-4" />
</Button>
</div>
)}
</div>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion web/lib/inngest/functions/crons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { inngest } from "../client";
export const staleServerCheck = inngest.createFunction(
{
id: "cron-stale-server-check",
triggers: [cron("*/5 * * * *")],
triggers: [cron("* * * * *")],
singleton: { mode: "skip" },
},
async ({ step }) => {
Expand Down
2 changes: 1 addition & 1 deletion web/lib/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
WORK_QUEUE_MAX_ATTEMPTS,
} from "@/lib/work-queue";

const STALE_THRESHOLD_MS = 120_000; // 2 minutes
const STALE_THRESHOLD_MS = 75_000; // 75 seconds

async function triggerRecoveryForOfflineServers(
offlineServerIds: string[],
Expand Down
Loading