diff --git a/web/app/(dashboard)/layout-client.tsx b/web/app/(dashboard)/layout-client.tsx index 12aa1b62..1bbc205b 100644 --- a/web/app/(dashboard)/layout-client.tsx +++ b/web/app/(dashboard)/layout-client.tsx @@ -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 = ( @@ -113,9 +119,20 @@ function DashboardHeader({ email }: { email: string }) { > - + - {email} + + + {name} + + + {email} + +
- +
{children}
diff --git a/web/components/metrics/metrics-history-charts.tsx b/web/components/metrics/metrics-history-charts.tsx index 811a4cd1..837f8028 100644 --- a/web/components/metrics/metrics-history-charts.tsx +++ b/web/components/metrics/metrics-history-charts.tsx @@ -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 { @@ -178,12 +178,7 @@ export function MetricsHistoryCharts({ description="The metrics API could not be reached. Current health data may still be available." /> ) : isLoading ? ( - + ) : !hasData ? (
+ ); +} + function MetricsStateCard({ icon: Icon, title, description, - loading = false, }: { icon: typeof Activity; title: string; description: string; - loading?: boolean; }) { return (
- {loading ? ( - - ) : ( - - )} +

{title}

diff --git a/web/components/server/server-services.tsx b/web/components/server/server-services.tsx index c348d9f5..1b1240fb 100644 --- a/web/components/server/server-services.tsx +++ b/web/components/server/server-services.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { Box, Layers } from "lucide-react"; +import { ArrowUpRight, Layers } from "lucide-react"; import { Card, CardContent, @@ -7,14 +7,6 @@ import { 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 }) { @@ -24,6 +16,8 @@ export async function ServerServices({ serverId }: { serverId: string }) { return null; } + const groups = groupServicesByProjectEnvironment(services); + return ( @@ -37,32 +31,62 @@ export async function ServerServices({ serverId }: { serverId: string }) { - - {services.map((service) => ( - - } - > - - - - - - {service.serviceName} - - - {service.projectName} / {service.environmentName} - - - +
+ {groups.map((group) => ( +
+
+

{group.projectName}

+ + {group.environmentName} + +
+
+ {group.services.map((service) => ( + + + {service.serviceName} + + + + ))} +
+
))} - +
); } + +type ServerService = Awaited>[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()); +} diff --git a/web/components/settings/member-settings.tsx b/web/components/settings/member-settings.tsx index f7fc5d1b..90cb0865 100644 --- a/web/components/settings/member-settings.tsx +++ b/web/components/settings/member-settings.tsx @@ -244,18 +244,20 @@ export function MemberSettings({ initialMembers, initialInvitations }: Props) { Reader )} -
- -
+ {member.role !== "admin" && ( +
+ +
+ )}
))} diff --git a/web/lib/inngest/functions/crons.ts b/web/lib/inngest/functions/crons.ts index 78983e04..92c3a068 100644 --- a/web/lib/inngest/functions/crons.ts +++ b/web/lib/inngest/functions/crons.ts @@ -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 }) => { diff --git a/web/lib/scheduler.ts b/web/lib/scheduler.ts index ccb5ed3c..c7177135 100644 --- a/web/lib/scheduler.ts +++ b/web/lib/scheduler.ts @@ -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[],