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