| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import { findErrorTraceId } from "@t3tools/client-runtime/errors"; |
| 2 | 2 | import { |
| 3 | type EnvironmentConnectionPresentation, |
| 3 | 4 | RelayConnectionRegistration, |
| 4 | 5 | RelayConnectionTarget, |
| 5 | 6 | } from "@t3tools/client-runtime/connection"; |
| ... | ... | @@ -10,7 +11,7 @@ import { |
| 10 | 11 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 11 | 12 | import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay"; |
| 12 | 13 | import * as Option from "effect/Option"; |
| 13 | | import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; |
| 14 | import { type ReactNode, useCallback, useEffect, useState } from "react"; |
| 14 | 15 | |
| 15 | 16 | import { environmentCatalog } from "~/connection/catalog"; |
| 16 | 17 | import { cn } from "~/lib/utils"; |
| ... | ... | @@ -22,6 +23,12 @@ import { ITEM_ROW_CLASSNAME, ITEM_ROW_INNER_CLASSNAME } from "../settings/itemRo |
| 22 | 23 | import { Button } from "../ui/button"; |
| 23 | 24 | import { Skeleton } from "../ui/skeleton"; |
| 24 | 25 | import { toastManager } from "../ui/toast"; |
| 26 | import { presentSavedCloudEnvironmentConnection } from "./cloudEnvironmentConnectionPresentation"; |
| 27 | |
| 28 | export interface SavedCloudEnvironmentConnection { |
| 29 | readonly environmentId: EnvironmentId; |
| 30 | readonly connection: EnvironmentConnectionPresentation; |
| 31 | } |
| 25 | 32 | |
| 26 | 33 | export function RemoteEnvironmentRowsSkeleton() { |
| 27 | 34 | return ( |
| ... | ... | @@ -40,19 +47,19 @@ export function RemoteEnvironmentRowsSkeleton() { |
| 40 | 47 | /** |
| 41 | 48 | * The user's T3 Connect environments from relay discovery, each with a |
| 42 | 49 | * Connect button. The primary environment is always excluded; already-saved |
| 43 | | * environments are hidden unless `showSavedAsConnected` renders them as |
| 44 | | * connected instead (used by onboarding, where the full device mesh should be |
| 45 | | * visible). |
| 50 | * environments are hidden unless `showSavedEnvironments` renders them with |
| 51 | * their live connection state (used by onboarding, where the full device mesh |
| 52 | * should be visible). |
| 46 | 53 | */ |
| 47 | 54 | export function CloudEnvironmentConnectRows({ |
| 48 | 55 | primaryEnvironmentId, |
| 49 | | savedEnvironmentIds, |
| 50 | | showSavedAsConnected = false, |
| 56 | savedEnvironments, |
| 57 | showSavedEnvironments = false, |
| 51 | 58 | empty = null, |
| 52 | 59 | }: { |
| 53 | 60 | readonly primaryEnvironmentId: EnvironmentId | null; |
| 54 | | readonly savedEnvironmentIds: ReadonlyArray<EnvironmentId>; |
| 55 | | readonly showSavedAsConnected?: boolean; |
| 61 | readonly savedEnvironments: ReadonlyArray<SavedCloudEnvironmentConnection>; |
| 62 | readonly showSavedEnvironments?: boolean; |
| 56 | 63 | readonly empty?: ReactNode; |
| 57 | 64 | }) { |
| 58 | 65 | const environmentsState = useRelayEnvironmentDiscovery(); |
| ... | ... | @@ -77,7 +84,9 @@ export function CloudEnvironmentConnectRows({ |
| 77 | 84 | const [connectingEnvironmentId, setConnectingEnvironmentId] = useState<EnvironmentId | null>( |
| 78 | 85 | null, |
| 79 | 86 | ); |
| 80 | | const savedIds = useMemo(() => new Set(savedEnvironmentIds), [savedEnvironmentIds]); |
| 87 | const savedById = new Map( |
| 88 | savedEnvironments.map((environment) => [environment.environmentId, environment]), |
| 89 | ); |
| 81 | 90 | |
| 82 | 91 | useEffect(() => { |
| 83 | 92 | void refreshRelayEnvironments(); |
| ... | ... | @@ -90,8 +99,8 @@ export function CloudEnvironmentConnectRows({ |
| 90 | 99 | if (result._tag === "Success") { |
| 91 | 100 | toastManager.add({ |
| 92 | 101 | type: "success", |
| 93 | | title: "Environment connected", |
| 94 | | description: `${environment.label} is available through T3 Connect.`, |
| 102 | title: "Environment added", |
| 103 | description: `Connecting to ${environment.label} through T3 Connect.`, |
| 95 | 104 | }); |
| 96 | 105 | return; |
| 97 | 106 | } |
| ... | ... | @@ -121,10 +130,10 @@ export function CloudEnvironmentConnectRows({ |
| 121 | 130 | const visibleEnvironments = [...environmentsState.environments.values()].filter( |
| 122 | 131 | ({ environment }) => |
| 123 | 132 | environment.environmentId !== primaryEnvironmentId && |
| 124 | | (showSavedAsConnected || !savedIds.has(environment.environmentId)), |
| 133 | (showSavedEnvironments || !savedById.has(environment.environmentId)), |
| 125 | 134 | ); |
| 126 | 135 | |
| 127 | | const standalone = showSavedAsConnected || savedEnvironmentIds.length === 0; |
| 136 | const standalone = showSavedEnvironments || savedEnvironments.length === 0; |
| 128 | 137 | |
| 129 | 138 | if ( |
| 130 | 139 | standalone && |
| ... | ... | @@ -163,31 +172,57 @@ export function CloudEnvironmentConnectRows({ |
| 163 | 172 | } |
| 164 | 173 | |
| 165 | 174 | return visibleEnvironments.map(({ environment, availability, error }) => { |
| 166 | | const alreadyConnected = savedIds.has(environment.environmentId); |
| 175 | const savedEnvironment = savedById.get(environment.environmentId); |
| 176 | const savedConnection = savedEnvironment |
| 177 | ? presentSavedCloudEnvironmentConnection(savedEnvironment.connection) |
| 178 | : null; |
| 179 | const dotClassName = savedConnection |
| 180 | ? savedConnection.tone === "connected" |
| 181 | ? "bg-success" |
| 182 | : savedConnection.tone === "connecting" |
| 183 | ? "bg-warning" |
| 184 | : savedConnection.tone === "error" |
| 185 | ? "bg-destructive" |
| 186 | : "bg-muted-foreground/35" |
| 187 | : availability === "online" |
| 188 | ? "bg-success" |
| 189 | : availability === "error" |
| 190 | ? "bg-destructive" |
| 191 | : availability === "checking" |
| 192 | ? "bg-warning" |
| 193 | : "bg-muted-foreground/35"; |
| 194 | const statusText = savedConnection |
| 195 | ? savedConnection.statusText |
| 196 | : availability === "online" |
| 197 | ? "Available · Relay online" |
| 198 | : availability === "offline" |
| 199 | ? "Available · Relay offline" |
| 200 | : availability === "checking" |
| 201 | ? "Available · Checking relay status…" |
| 202 | : (Option.getOrNull(error)?.message ?? "Available · Relay status unavailable"); |
| 167 | 203 | return ( |
| 168 | 204 | <div key={environment.environmentId} className={ITEM_ROW_CLASSNAME}> |
| 169 | 205 | <div className={ITEM_ROW_INNER_CLASSNAME}> |
| 170 | 206 | <div className="min-w-0"> |
| 171 | 207 | <div className="flex items-center gap-2"> |
| 172 | 208 | <ConnectionStatusDot |
| 173 | | dotClassName={ |
| 174 | | availability === "online" |
| 175 | | ? "bg-success" |
| 176 | | : availability === "error" |
| 177 | | ? "bg-destructive" |
| 178 | | : availability === "checking" |
| 179 | | ? "bg-warning" |
| 180 | | : "bg-muted-foreground/35" |
| 209 | dotClassName={dotClassName} |
| 210 | pingClassName={ |
| 211 | savedConnection?.tone === "connecting" || |
| 212 | (savedConnection === null && availability === "checking") |
| 213 | ? "bg-warning/60 duration-2000" |
| 214 | : null |
| 181 | 215 | } |
| 182 | | pingClassName={availability === "checking" ? "bg-warning/60 duration-2000" : null} |
| 183 | 216 | tooltipText={ |
| 184 | | availability === "online" |
| 185 | | ? "Relay online" |
| 186 | | : availability === "offline" |
| 187 | | ? "Relay offline" |
| 188 | | : availability === "checking" |
| 189 | | ? "Checking relay status" |
| 190 | | : (Option.getOrNull(error)?.message ?? "Relay status unavailable") |
| 217 | savedConnection |
| 218 | ? savedConnection.statusText |
| 219 | : availability === "online" |
| 220 | ? "Relay online" |
| 221 | : availability === "offline" |
| 222 | ? "Relay offline" |
| 223 | : availability === "checking" |
| 224 | ? "Checking relay status" |
| 225 | : (Option.getOrNull(error)?.message ?? "Relay status unavailable") |
| 191 | 226 | } |
| 192 | 227 | /> |
| 193 | 228 | <p className="truncate text-sm font-medium">{environment.label}</p> |
| ... | ... | @@ -195,21 +230,19 @@ export function CloudEnvironmentConnectRows({ |
| 195 | 230 | <p |
| 196 | 231 | className={cn( |
| 197 | 232 | "mt-1 truncate text-xs", |
| 198 | | availability === "error" ? "text-destructive" : "text-muted-foreground", |
| 233 | savedConnection?.tone === "error" || |
| 234 | (savedConnection?.tone === "connecting" && savedEnvironment?.connection.error) || |
| 235 | (savedConnection === null && availability === "error") |
| 236 | ? "text-destructive" |
| 237 | : "text-muted-foreground", |
| 199 | 238 | )} |
| 200 | 239 | > |
| 201 | | {availability === "online" |
| 202 | | ? "Available · Relay online" |
| 203 | | : availability === "offline" |
| 204 | | ? "Available · Relay offline" |
| 205 | | : availability === "checking" |
| 206 | | ? "Available · Checking relay status…" |
| 207 | | : (Option.getOrNull(error)?.message ?? "Available · Relay status unavailable")} |
| 240 | {statusText} |
| 208 | 241 | </p> |
| 209 | 242 | </div> |
| 210 | | {alreadyConnected ? ( |
| 243 | {savedConnection ? ( |
| 211 | 244 | <Button size="sm" variant="outline" disabled> |
| 212 | | Connected |
| 245 | {savedConnection.buttonLabel} |
| 213 | 246 | </Button> |
| 214 | 247 | ) : ( |
| 215 | 248 | <Button |