import { findErrorTraceId } from "@t3tools/client-runtime/errors";import { isAtomCommandInterrupted, squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime";import type { EnvironmentId } from "@t3tools/contracts";import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";import { ServerIcon } from "lucide-react";import { useRef, useState } from "react"; import { deregisterManagedRelayEnvironmentCommand, useManagedRelayEnvironments,} from "../../cloud/managedRelayState";import { useAtomCommand } from "../../state/use-atom-command";import { Button } from "../ui/button";import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";import { toastManager } from "../ui/toast";import { ClerkUserProfilePage, ClerkUserProfileRefreshButton, ClerkUserProfileRow,} from "./ClerkUserProfilePage"; const linkedAtFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }); function linkedAtLabel(value: string): string { const linkedAt = new Date(value); return Number.isNaN(linkedAt.getTime()) ? "Link date unavailable" : `Linked ${linkedAtFormatter.format(linkedAt)}`;} function endpointLabel(environment: RelayClientEnvironmentRecord): string { return environment.endpoint.providerKind === "cloudflare_tunnel" ? "Managed tunnel" : "Activity publishing only";} export function T3ConnectEnvironmentRow(props: { readonly environment: RelayClientEnvironmentRecord; readonly confirmationOpen: boolean; readonly mutationPending: boolean; readonly onConfirmationChange: (open: boolean) => void; readonly onDeregister: (environment: RelayClientEnvironmentRecord) => void;}) { const { environment } = props; return ( <ClerkUserProfileRow icon={<ServerIcon className="size-4" />}> <Collapsible open={props.confirmationOpen} onOpenChange={props.onConfirmationChange}> <div className="flex items-start gap-4"> <div className="min-w-0 flex-1"> <h3 className="truncate text-[0.8125rem] leading-[1.125rem] font-medium text-foreground"> {environment.label} </h3> <p className="mt-1 text-xs leading-[1.125rem] text-muted-foreground"> {linkedAtLabel(environment.linkedAt)} · {endpointLabel(environment)} </p> </div> <CollapsibleTrigger render={ <Button size="sm" variant="destructive-outline" className="text-[0.8125rem]" disabled={props.mutationPending} > Deregister </Button> } /> </div> <CollapsiblePanel> <div className="pt-3"> <div className="rounded-lg border border-input bg-muted/32 px-5 py-4 shadow-xs/5" role="group" aria-label={`Confirm deregistration of ${environment.label}`} > <h4 className="text-[0.8125rem] leading-[1.125rem] font-semibold text-foreground"> Deregister server </h4> <p className="mt-1 text-[0.8125rem] leading-[1.125rem] text-muted-foreground"> “{environment.label}” will be removed from this account. </p> <p className="mt-4 max-w-xl text-[0.8125rem] leading-[1.125rem] text-muted-foreground"> T3 Connect access will be revoked, any managed tunnel will be removed, and a host space will become available. Local connections on your devices are not changed. </p> <div className="mt-4 flex justify-end gap-2"> <Button size="sm" variant="ghost" className="text-[0.8125rem]" disabled={props.mutationPending} onClick={() => props.onConfirmationChange(false)} > Cancel </Button> <Button size="sm" variant="destructive" className="text-[0.8125rem]" disabled={props.mutationPending} onClick={() => props.onDeregister(environment)} > {props.mutationPending ? "Deregistering…" : "Deregister"} </Button> </div> </div> </div> </CollapsiblePanel> </Collapsible> </ClerkUserProfileRow> );} export function T3ConnectUserProfilePage() { const environmentsState = useManagedRelayEnvironments(); const deregisterEnvironment = useAtomCommand(deregisterManagedRelayEnvironmentCommand, { reportFailure: false, }); const [deregisteringEnvironmentId, setDeregisteringEnvironmentId] = useState<EnvironmentId | null>(null); const [confirmingEnvironmentId, setConfirmingEnvironmentId] = useState<EnvironmentId | null>( null, ); const mutationPendingRef = useRef(false); const [removedEnvironments, setRemovedEnvironments] = useState<{ readonly accountId: string | null; readonly linkedAtById: ReadonlyMap<EnvironmentId, string>; }>({ accountId: null, linkedAtById: new Map() }); const handleDeregister = async (environment: RelayClientEnvironmentRecord) => { const accountId = environmentsState.accountId; if (!accountId || mutationPendingRef.current) return; mutationPendingRef.current = true; setDeregisteringEnvironmentId(environment.environmentId); const result = await deregisterEnvironment({ accountId, environmentId: environment.environmentId, }); mutationPendingRef.current = false; setDeregisteringEnvironmentId(null); if (result._tag === "Success") { setConfirmingEnvironmentId(null); setRemovedEnvironments((current) => { const linkedAtById = new Map(current.accountId === accountId ? current.linkedAtById : []); linkedAtById.set(environment.environmentId, environment.linkedAt); return { accountId, linkedAtById }; }); environmentsState.refresh(); toastManager.add({ type: "success", title: "Server deregistered", description: "T3 Connect access was revoked and a host space is now available.", }); return; } if (isAtomCommandInterrupted(result)) return; const cause = squashAtomCommandFailure(result); const message = cause instanceof Error ? cause.message : "Could not deregister the server."; const traceId = findErrorTraceId(cause); console.error("[t3-connect] Could not deregister environment", { environmentId: environment.environmentId, message, traceId, cause, }); toastManager.add({ type: "error", title: "Could not deregister server", description: message, data: traceId ? { secondaryActionProps: { children: "Copy trace ID", onClick: () => void navigator.clipboard?.writeText(traceId), }, } : undefined, }); }; const removedEnvironmentLinkedAt = removedEnvironments.accountId === environmentsState.accountId ? removedEnvironments.linkedAtById : new Map<EnvironmentId, string>(); const environments = (environmentsState.data ?? []).filter( (environment) => removedEnvironmentLinkedAt.get(environment.environmentId) !== environment.linkedAt, ); const isInitialLoad = !environmentsState.accountId || (environmentsState.data === null && !environmentsState.error); return ( <ClerkUserProfilePage title="T3 Connect" description="Environments registered to your account. Connections on this device are managed in Settings." action={ <ClerkUserProfileRefreshButton disabled={deregisteringEnvironmentId !== null} isPending={environmentsState.isPending} onClick={environmentsState.refresh} /> } > <div> {environmentsState.error ? ( <div className="mb-4 border-t border-destructive/35 py-3 text-[0.8125rem]" role="alert"> <p className="font-medium text-destructive-foreground"> Could not load T3 Connect environments </p> <p className="mt-1 text-xs text-muted-foreground">{environmentsState.error}</p> </div> ) : null} {isInitialLoad ? ( <p className="border-t py-4 text-[0.8125rem] text-muted-foreground" role="status"> Loading environments… </p> ) : environments.length > 0 ? ( <ul className="border-t"> {environments.map((environment) => ( <T3ConnectEnvironmentRow key={environment.environmentId} environment={environment} confirmationOpen={confirmingEnvironmentId === environment.environmentId} mutationPending={deregisteringEnvironmentId !== null} onConfirmationChange={(open) => setConfirmingEnvironmentId(open ? environment.environmentId : null) } onDeregister={(selected) => void handleDeregister(selected)} /> ))} </ul> ) : environmentsState.error ? null : ( <Empty className="min-h-64 gap-4 border-t px-6 py-10 md:p-10"> <EmptyMedia className="mb-0" variant="icon"> <ServerIcon /> </EmptyMedia> <EmptyHeader> <EmptyTitle className="text-[1.0625rem] leading-6"> No T3 Connect environments </EmptyTitle> <EmptyDescription className="text-[0.8125rem] leading-[1.125rem]"> Link an environment from its local Settings to make it available through T3 Connect. </EmptyDescription> </EmptyHeader> </Empty> )} </div> </ClerkUserProfilePage> );}
import { findErrorTraceId } from "@t3tools/client-runtime/errors";
import {
isAtomCommandInterrupted,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import type { EnvironmentId } from "@t3tools/contracts";
import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
import { ServerIcon } from "lucide-react";
import { useRef, useState } from "react";
import {
deregisterManagedRelayEnvironmentCommand,
useManagedRelayEnvironments,
} from "../../cloud/managedRelayState";
import { useAtomCommand } from "../../state/use-atom-command";
import { Button } from "../ui/button";
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";
import { toastManager } from "../ui/toast";
import {
ClerkUserProfilePage,
ClerkUserProfileRefreshButton,
ClerkUserProfileRow,
} from "./ClerkUserProfilePage";
const linkedAtFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium" });
function linkedAtLabel(value: string): string {
const linkedAt = new Date(value);
return Number.isNaN(linkedAt.getTime())
? "Link date unavailable"
: `Linked ${linkedAtFormatter.format(linkedAt)}`;
}
function endpointLabel(environment: RelayClientEnvironmentRecord): string {
return environment.endpoint.providerKind === "cloudflare_tunnel"
? "Managed tunnel"
: "Activity publishing only";
}
export function T3ConnectEnvironmentRow(props: {
readonly environment: RelayClientEnvironmentRecord;
readonly confirmationOpen: boolean;
readonly mutationPending: boolean;
readonly onConfirmationChange: (open: boolean) => void;
readonly onDeregister: (environment: RelayClientEnvironmentRecord) => void;
}) {
const { environment } = props;
return (
<ClerkUserProfileRow icon={<ServerIcon className="size-4" />}>
<Collapsible open={props.confirmationOpen} onOpenChange={props.onConfirmationChange}>
<div className="flex items-start gap-4">
<div className="min-w-0 flex-1">
<h3 className="truncate text-[0.8125rem] leading-[1.125rem] font-medium text-foreground">
{environment.label}
</h3>
<p className="mt-1 text-xs leading-[1.125rem] text-muted-foreground">
{linkedAtLabel(environment.linkedAt)} · {endpointLabel(environment)}
</p>
</div>
<CollapsibleTrigger
render={
<Button
size="sm"
variant="destructive-outline"
className="text-[0.8125rem]"
disabled={props.mutationPending}
>
Deregister
</Button>
}
/>
</div>
<CollapsiblePanel>
<div className="pt-3">
<div
className="rounded-lg border border-input bg-muted/32 px-5 py-4 shadow-xs/5"
role="group"
aria-label={`Confirm deregistration of ${environment.label}`}
>
<h4 className="text-[0.8125rem] leading-[1.125rem] font-semibold text-foreground">
Deregister server
</h4>
<p className="mt-1 text-[0.8125rem] leading-[1.125rem] text-muted-foreground">
“{environment.label}” will be removed from this account.
</p>
<p className="mt-4 max-w-xl text-[0.8125rem] leading-[1.125rem] text-muted-foreground">
T3 Connect access will be revoked, any managed tunnel will be removed, and a host
space will become available. Local connections on your devices are not changed.
</p>
<div className="mt-4 flex justify-end gap-2">
<Button
size="sm"
variant="ghost"
className="text-[0.8125rem]"
disabled={props.mutationPending}
onClick={() => props.onConfirmationChange(false)}
>
Cancel
</Button>
<Button
size="sm"
variant="destructive"
className="text-[0.8125rem]"
disabled={props.mutationPending}
onClick={() => props.onDeregister(environment)}
>
{props.mutationPending ? "Deregistering…" : "Deregister"}
</Button>
</div>
</div>
</div>
</CollapsiblePanel>
</Collapsible>
</ClerkUserProfileRow>
);
}
export function T3ConnectUserProfilePage() {
const environmentsState = useManagedRelayEnvironments();
const deregisterEnvironment = useAtomCommand(deregisterManagedRelayEnvironmentCommand, {
reportFailure: false,
});
const [deregisteringEnvironmentId, setDeregisteringEnvironmentId] =
useState<EnvironmentId | null>(null);
const [confirmingEnvironmentId, setConfirmingEnvironmentId] = useState<EnvironmentId | null>(
null,
);
const mutationPendingRef = useRef(false);
const [removedEnvironments, setRemovedEnvironments] = useState<{
readonly accountId: string | null;
readonly linkedAtById: ReadonlyMap<EnvironmentId, string>;
}>({ accountId: null, linkedAtById: new Map() });
const handleDeregister = async (environment: RelayClientEnvironmentRecord) => {
const accountId = environmentsState.accountId;
if (!accountId || mutationPendingRef.current) return;
mutationPendingRef.current = true;
setDeregisteringEnvironmentId(environment.environmentId);
const result = await deregisterEnvironment({
accountId,
environmentId: environment.environmentId,
});
mutationPendingRef.current = false;
setDeregisteringEnvironmentId(null);
if (result._tag === "Success") {
setConfirmingEnvironmentId(null);
setRemovedEnvironments((current) => {
const linkedAtById = new Map(current.accountId === accountId ? current.linkedAtById : []);
linkedAtById.set(environment.environmentId, environment.linkedAt);
return { accountId, linkedAtById };
});
environmentsState.refresh();
toastManager.add({
type: "success",
title: "Server deregistered",
description: "T3 Connect access was revoked and a host space is now available.",
});
return;
}
if (isAtomCommandInterrupted(result)) return;
const cause = squashAtomCommandFailure(result);
const message = cause instanceof Error ? cause.message : "Could not deregister the server.";
const traceId = findErrorTraceId(cause);
console.error("[t3-connect] Could not deregister environment", {
environmentId: environment.environmentId,
message,
traceId,
cause,
});
toastManager.add({
type: "error",
title: "Could not deregister server",
description: message,
data: traceId
? {
secondaryActionProps: {
children: "Copy trace ID",
onClick: () => void navigator.clipboard?.writeText(traceId),
},
}
: undefined,
});
};
const removedEnvironmentLinkedAt =
removedEnvironments.accountId === environmentsState.accountId
? removedEnvironments.linkedAtById
: new Map<EnvironmentId, string>();
const environments = (environmentsState.data ?? []).filter(
(environment) =>
removedEnvironmentLinkedAt.get(environment.environmentId) !== environment.linkedAt,
);
const isInitialLoad =
!environmentsState.accountId || (environmentsState.data === null && !environmentsState.error);
return (
<ClerkUserProfilePage
title="T3 Connect"
description="Environments registered to your account. Connections on this device are managed in Settings."
action={
<ClerkUserProfileRefreshButton
disabled={deregisteringEnvironmentId !== null}
isPending={environmentsState.isPending}
onClick={environmentsState.refresh}
/>
}
>
<div>
{environmentsState.error ? (
<div className="mb-4 border-t border-destructive/35 py-3 text-[0.8125rem]" role="alert">
<p className="font-medium text-destructive-foreground">
Could not load T3 Connect environments
</p>
<p className="mt-1 text-xs text-muted-foreground">{environmentsState.error}</p>
</div>
) : null}
{isInitialLoad ? (
<p className="border-t py-4 text-[0.8125rem] text-muted-foreground" role="status">
Loading environments…
</p>
) : environments.length > 0 ? (
<ul className="border-t">
{environments.map((environment) => (
<T3ConnectEnvironmentRow
key={environment.environmentId}
environment={environment}
confirmationOpen={confirmingEnvironmentId === environment.environmentId}
mutationPending={deregisteringEnvironmentId !== null}
onConfirmationChange={(open) =>
setConfirmingEnvironmentId(open ? environment.environmentId : null)
}
onDeregister={(selected) => void handleDeregister(selected)}
/>
))}
</ul>
) : environmentsState.error ? null : (
<Empty className="min-h-64 gap-4 border-t px-6 py-10 md:p-10">
<EmptyMedia className="mb-0" variant="icon">
<ServerIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle className="text-[1.0625rem] leading-6">
No T3 Connect environments
</EmptyTitle>
<EmptyDescription className="text-[0.8125rem] leading-[1.125rem]">
Link an environment from its local Settings to make it available through T3 Connect.
</EmptyDescription>
</EmptyHeader>
</Empty>
)}
</div>
</ClerkUserProfilePage>
);
}