| ... | ... | @@ -6,6 +6,7 @@ import { |
| 6 | 6 | TerminalIcon, |
| 7 | 7 | } from "lucide-react"; |
| 8 | 8 | import { useAtomValue } from "@effect/atom-react"; |
| 9 | import { useLocation } from "@tanstack/react-router"; |
| 9 | 10 | import { Atom } from "effect/unstable/reactivity"; |
| 10 | 11 | import { |
| 11 | 12 | type KeyboardEvent, |
| ... | ... | @@ -41,7 +42,7 @@ import { |
| 41 | 42 | type EnvironmentId, |
| 42 | 43 | resolveEnvironmentMachineKind, |
| 43 | 44 | } from "@t3tools/contracts"; |
| 44 | | import { connectionStatusText } from "@t3tools/client-runtime/connection"; |
| 45 | import { connectionStatusText, connectionStatusTitle } from "@t3tools/client-runtime/connection"; |
| 45 | 46 | import { |
| 46 | 47 | isAtomCommandInterrupted, |
| 47 | 48 | squashAtomCommandFailure, |
| ... | ... | @@ -67,7 +68,7 @@ import { |
| 67 | 68 | } from "./settingsLayout"; |
| 68 | 69 | import { searchableSetting } from "./settingsSearch"; |
| 69 | 70 | import { EnvironmentIconPicker } from "./EnvironmentIconPicker"; |
| 70 | | import { LoadBalancingSettings } from "./LoadBalancingSettings"; |
| 71 | import { LoadBalancingPreference, LoadBalancingSettings } from "./LoadBalancingSettings"; |
| 71 | 72 | import { GitHubRoutingSettings } from "./GitHubRoutingSettings"; |
| 72 | 73 | import { Input } from "../ui/input"; |
| 73 | 74 | import { CommandShortcut } from "../ui/command"; |
| ... | ... | @@ -157,7 +158,7 @@ import { |
| 157 | 158 | import { requestConfirmDialog } from "~/confirmDialog"; |
| 158 | 159 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 159 | 160 | import { primaryServerKeybindingsAtom, serverEnvironment } from "~/state/server"; |
| 160 | | import { ConnectionStatusDot } from "../ConnectionStatusDot"; |
| 161 | import { ConnectionStatusDot, connectionPhaseDotClassName } from "../ConnectionStatusDot"; |
| 161 | 162 | import { |
| 162 | 163 | ServerUpdateAction, |
| 163 | 164 | ServerUpdateProgress, |
| ... | ... | @@ -1498,37 +1499,40 @@ function SavedBackendListRow({ |
| 1498 | 1499 | <div className={cn(ITEM_ROW_CLASSNAME, !enabled && "opacity-60")}> |
| 1499 | 1500 | <div className={ITEM_ROW_INNER_CLASSNAME}> |
| 1500 | 1501 | <div className="min-w-0 flex-1 space-y-1"> |
| 1501 | | <div className="flex min-h-5 items-center gap-1.5"> |
| 1502 | | <ConnectionStatusDot |
| 1503 | | tooltipText={statusTooltip} |
| 1504 | | dotClassName={stateDotClassName} |
| 1505 | | pingClassName={ |
| 1506 | | enabled && (connectionState === "connecting" || connectionState === "reconnecting") |
| 1507 | | ? "bg-warning/60 duration-2000" |
| 1508 | | : null |
| 1509 | | } |
| 1510 | | /> |
| 1511 | | <EnvironmentMachineIcon |
| 1512 | | aria-hidden |
| 1513 | | kind={resolveEnvironmentMachineKind(environment.serverConfig)} |
| 1514 | | className="size-3.5 shrink-0 text-muted-foreground" |
| 1515 | | /> |
| 1516 | | <h3 className="min-w-0 truncate text-sm font-medium text-foreground"> |
| 1517 | | {environment.label} |
| 1518 | | </h3> |
| 1502 | <div className="flex flex-wrap items-center gap-x-3 gap-y-2"> |
| 1503 | <div className="flex min-h-5 min-w-0 max-w-full items-center gap-1.5"> |
| 1504 | <ConnectionStatusDot |
| 1505 | tooltipText={statusTooltip} |
| 1506 | dotClassName={stateDotClassName} |
| 1507 | pingClassName={ |
| 1508 | enabled && |
| 1509 | (connectionState === "connecting" || connectionState === "reconnecting") |
| 1510 | ? "bg-warning/60 duration-2000" |
| 1511 | : null |
| 1512 | } |
| 1513 | /> |
| 1514 | <EnvironmentMachineIcon |
| 1515 | aria-hidden |
| 1516 | kind={resolveEnvironmentMachineKind(environment.serverConfig)} |
| 1517 | className="size-3.5 shrink-0 text-muted-foreground" |
| 1518 | /> |
| 1519 | <h3 className="min-w-0 truncate text-sm font-medium text-foreground"> |
| 1520 | {environment.label} |
| 1521 | </h3> |
| 1522 | </div> |
| 1523 | {isConnected ? ( |
| 1524 | <div className="w-full sm:w-auto"> |
| 1525 | <EnvironmentIconPicker |
| 1526 | environmentId={environmentId} |
| 1527 | serverConfig={environment.serverConfig} |
| 1528 | size="xs" |
| 1529 | /> |
| 1530 | </div> |
| 1531 | ) : null} |
| 1519 | 1532 | </div> |
| 1520 | 1533 | {metadataBits.length > 0 ? ( |
| 1521 | 1534 | <p className="truncate text-xs text-muted-foreground">{metadataBits.join(" · ")}</p> |
| 1522 | 1535 | ) : null} |
| 1523 | | {isConnected ? ( |
| 1524 | | <div className="pt-1"> |
| 1525 | | <EnvironmentIconPicker |
| 1526 | | environmentId={environmentId} |
| 1527 | | serverConfig={environment.serverConfig} |
| 1528 | | size="xs" |
| 1529 | | /> |
| 1530 | | </div> |
| 1531 | | ) : null} |
| 1532 | 1536 | {serverUpdateState.status !== "idle" ? ( |
| 1533 | 1537 | <div className="max-w-md"> |
| 1534 | 1538 | <ServerUpdateProgress state={serverUpdateState} /> |
| ... | ... | @@ -1586,12 +1590,12 @@ function SavedBackendListRow({ |
| 1586 | 1590 | <TooltipTrigger |
| 1587 | 1591 | render={ |
| 1588 | 1592 | <Button size="xs" variant="outline" disabled> |
| 1589 | | Managed above |
| 1593 | Managed locally |
| 1590 | 1594 | </Button> |
| 1591 | 1595 | } |
| 1592 | 1596 | /> |
| 1593 | 1597 | <TooltipPopup side="top" className="max-w-80 whitespace-pre-wrap leading-tight"> |
| 1594 | | The WSL backend is managed by the WSL setting above — turn it on or off there. |
| 1598 | Select the primary environment to turn the WSL backend on or off. |
| 1595 | 1599 | </TooltipPopup> |
| 1596 | 1600 | </Tooltip> |
| 1597 | 1601 | ) : ( |
| ... | ... | @@ -1813,6 +1817,19 @@ export function ConnectionsSettings() { |
| 1813 | 1817 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 1814 | 1818 | const { environments } = useEnvironments(); |
| 1815 | 1819 | const primaryEnvironment = usePrimaryEnvironment(); |
| 1820 | const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<EnvironmentId | null>(null); |
| 1821 | const searchTargetId = useLocation({ select: (location) => location.hash.replace(/^#/, "") }); |
| 1822 | const [handledSearchTargetId, setHandledSearchTargetId] = useState<string | null>(null); |
| 1823 | if (primaryEnvironment && handledSearchTargetId !== searchTargetId) { |
| 1824 | setHandledSearchTargetId(searchTargetId); |
| 1825 | if (["connections-environment", "wsl-backend"].includes(searchTargetId)) { |
| 1826 | setSelectedEnvironmentId(primaryEnvironment.environmentId); |
| 1827 | } |
| 1828 | } |
| 1829 | const selectedEnvironment = |
| 1830 | environments.find((environment) => environment.environmentId === selectedEnvironmentId) ?? |
| 1831 | primaryEnvironment ?? |
| 1832 | environments[0]; |
| 1816 | 1833 | const connectPairing = useAtomCommand(connectPairingAtom, { reportFailure: false }); |
| 1817 | 1834 | const connectSshEnvironment = useAtomCommand(connectSshEnvironmentAtom, { |
| 1818 | 1835 | reportFailure: false, |
| ... | ... | @@ -3235,8 +3252,8 @@ export function ConnectionsSettings() { |
| 3235 | 3252 | /> |
| 3236 | 3253 | ); |
| 3237 | 3254 | |
| 3238 | | return ( |
| 3239 | | <SettingsPageContainer> |
| 3255 | const primarySettings = ( |
| 3256 | <> |
| 3240 | 3257 | {canManageLocalBackend ? ( |
| 3241 | 3258 | <> |
| 3242 | 3259 | <SettingsSection |
| ... | ... | @@ -3324,6 +3341,7 @@ export function ConnectionsSettings() { |
| 3324 | 3341 | {isLocalBackendRemotelyReachable ? ( |
| 3325 | 3342 | <SettingsSection |
| 3326 | 3343 | title="Authorized clients" |
| 3344 | className="[&>div:first-child]:flex-wrap" |
| 3327 | 3345 | headerAction={ |
| 3328 | 3346 | <AuthorizedClientsHeaderAction |
| 3329 | 3347 | onPairingLinkCreated={handlePairingLinkCreated} |
| ... | ... | @@ -3626,9 +3644,15 @@ export function ConnectionsSettings() { |
| 3626 | 3644 | <CloudLinkRow canManageRelay={canManageRelay} /> |
| 3627 | 3645 | </SettingsSection> |
| 3628 | 3646 | )} |
| 3647 | </> |
| 3648 | ); |
| 3629 | 3649 | |
| 3650 | return ( |
| 3651 | <SettingsPageContainer width="wide"> |
| 3630 | 3652 | <SettingsSection |
| 3631 | 3653 | {...searchableSetting("remote-environments")} |
| 3654 | title="Environments" |
| 3655 | variant="plain" |
| 3632 | 3656 | headerAction={ |
| 3633 | 3657 | <div className="flex items-center gap-1"> |
| 3634 | 3658 | {savedServerUpdateTargets.length > 0 ? ( |
| ... | ... | @@ -3701,21 +3725,117 @@ export function ConnectionsSettings() { |
| 3701 | 3725 | </div> |
| 3702 | 3726 | } |
| 3703 | 3727 | > |
| 3704 | | {savedEnvironments.map((environment) => ( |
| 3705 | | <SavedBackendListRow |
| 3706 | | key={environment.environmentId} |
| 3707 | | environment={environment} |
| 3708 | | removingEnvironmentId={removingSavedEnvironmentId} |
| 3709 | | onSetEnabled={handleSetSavedBackendEnabled} |
| 3710 | | onRemove={handleRemoveSavedBackend} |
| 3711 | | /> |
| 3712 | | ))} |
| 3728 | <div className="overflow-hidden rounded-xl border border-border/60 bg-card/40 shadow-xs/5 lg:grid lg:h-[min(34rem,calc(100dvh-18rem))] lg:min-h-[28rem] lg:grid-cols-[17rem_minmax(0,1fr)]"> |
| 3729 | <div className="border-b border-border/60 bg-muted/10 lg:flex lg:min-h-0 lg:flex-col lg:border-r lg:border-b-0"> |
| 3730 | <ScrollArea |
| 3731 | scrollFade |
| 3732 | chainVerticalScroll |
| 3733 | className="max-h-64 lg:max-h-none lg:min-h-0 lg:flex-1" |
| 3734 | > |
| 3735 | <div className="divide-y divide-border/50" aria-label="Environments"> |
| 3736 | {(primaryEnvironment |
| 3737 | ? [primaryEnvironment, ...savedEnvironments] |
| 3738 | : savedEnvironments |
| 3739 | ).map((environment) => { |
| 3740 | const selected = environment.environmentId === selectedEnvironment?.environmentId; |
| 3741 | const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget"; |
| 3742 | return ( |
| 3743 | <button |
| 3744 | key={environment.environmentId} |
| 3745 | type="button" |
| 3746 | aria-label={"Select " + environment.label} |
| 3747 | aria-pressed={selected} |
| 3748 | onClick={() => setSelectedEnvironmentId(environment.environmentId)} |
| 3749 | className={cn( |
| 3750 | "flex min-h-18 w-full cursor-pointer items-start gap-3 px-3 py-3 text-left outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring sm:px-4", |
| 3751 | selected ? "bg-muted/45" : "hover:bg-muted/25", |
| 3752 | !environment.entry.enabled && !selected && "opacity-60", |
| 3753 | )} |
| 3754 | > |
| 3755 | <EnvironmentMachineIcon |
| 3756 | aria-hidden |
| 3757 | kind={resolveEnvironmentMachineKind(environment.serverConfig)} |
| 3758 | className="mt-0.5 size-4 shrink-0 text-muted-foreground" |
| 3759 | /> |
| 3760 | <span className="min-w-0 flex-1 space-y-1"> |
| 3761 | <span className="flex items-center gap-2"> |
| 3762 | <span className="truncate text-sm font-medium">{environment.label}</span> |
| 3763 | {isPrimary ? ( |
| 3764 | <span className="shrink-0 text-[10px] text-muted-foreground"> |
| 3765 | Primary |
| 3766 | </span> |
| 3767 | ) : null} |
| 3768 | </span> |
| 3769 | <Tooltip> |
| 3770 | <TooltipTrigger |
| 3771 | render={ |
| 3772 | <span className="block truncate text-xs text-muted-foreground" /> |
| 3773 | } |
| 3774 | > |
| 3775 | {environment.displayUrl ?? "T3 Connect"} |
| 3776 | </TooltipTrigger> |
| 3777 | <TooltipPopup>{environment.displayUrl ?? "T3 Connect"}</TooltipPopup> |
| 3778 | </Tooltip> |
| 3779 | <span className="flex items-center gap-1.5 text-xs text-muted-foreground"> |
| 3780 | <span |
| 3781 | aria-hidden |
| 3782 | className={cn( |
| 3783 | "size-1.5 shrink-0 rounded-full", |
| 3784 | !environment.entry.enabled |
| 3785 | ? "bg-muted-foreground/40" |
| 3786 | : connectionPhaseDotClassName(environment.connection.phase), |
| 3787 | )} |
| 3788 | /> |
| 3789 | {environment.entry.enabled |
| 3790 | ? connectionStatusTitle(environment.connection) |
| 3791 | : "Off"} |
| 3792 | </span> |
| 3793 | </span> |
| 3794 | </button> |
| 3795 | ); |
| 3796 | })} |
| 3797 | </div> |
| 3798 | </ScrollArea> |
| 3799 | </div> |
| 3800 | <ScrollArea |
| 3801 | key={selectedEnvironment?.environmentId} |
| 3802 | scrollFade |
| 3803 | chainVerticalScroll |
| 3804 | className="min-w-0 lg:min-h-0 lg:h-full" |
| 3805 | > |
| 3806 | {selectedEnvironment ? ( |
| 3807 | <div className="space-y-6 p-4"> |
| 3808 | {selectedEnvironment.entry.target._tag === "PrimaryConnectionTarget" ? ( |
| 3809 | primarySettings |
| 3810 | ) : ( |
| 3811 | <SettingsSection title="Connection"> |
| 3812 | <SavedBackendListRow |
| 3813 | key={selectedEnvironment.environmentId} |
| 3814 | environment={selectedEnvironment} |
| 3815 | removingEnvironmentId={removingSavedEnvironmentId} |
| 3816 | onSetEnabled={handleSetSavedBackendEnabled} |
| 3817 | onRemove={handleRemoveSavedBackend} |
| 3818 | /> |
| 3819 | </SettingsSection> |
| 3820 | )} |
| 3821 | {selectedEnvironment.entry.enabled && loadBalancingEnvironments.length > 1 ? ( |
| 3822 | <SettingsSection title="Thread placement"> |
| 3823 | <LoadBalancingPreference environment={selectedEnvironment} /> |
| 3824 | </SettingsSection> |
| 3825 | ) : null} |
| 3826 | <GitHubRoutingSettings |
| 3827 | environments={environments} |
| 3828 | selectedEnvironmentId={selectedEnvironment.environmentId} |
| 3829 | /> |
| 3830 | </div> |
| 3831 | ) : null} |
| 3832 | </ScrollArea> |
| 3833 | </div> |
| 3713 | 3834 | <CloudRemoteEnvironmentRows |
| 3714 | 3835 | primaryEnvironmentId={primaryEnvironmentId} |
| 3715 | 3836 | savedEnvironments={savedEnvironments} |
| 3716 | 3837 | /> |
| 3717 | 3838 | </SettingsSection> |
| 3718 | | <GitHubRoutingSettings environments={environments} /> |
| 3719 | 3839 | <LoadBalancingSettings environments={loadBalancingEnvironments} /> |
| 3720 | 3840 | </SettingsPageContainer> |
| 3721 | 3841 | ); |