260 lines · 9.9 KB · TypeScript
RawDownload
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>  );}