| ... | @@ -6,6 +6,7 @@ import { | ... | @@ -6,6 +6,7 @@ import { |
| 6 | TerminalIcon, | 6 | TerminalIcon, |
| 7 | } from "lucide-react"; | 7 | } from "lucide-react"; |
| 8 | import { useAtomValue } from "@effect/atom-react"; | 8 | import { useAtomValue } from "@effect/atom-react"; |
| | 9 | import { useLocation } from "@tanstack/react-router"; |
| 9 | import { Atom } from "effect/unstable/reactivity"; | 10 | import { Atom } from "effect/unstable/reactivity"; |
| 10 | import { | 11 | import { |
| 11 | type KeyboardEvent, | 12 | type KeyboardEvent, |
| ... | @@ -41,7 +42,7 @@ import { | ... | @@ -41,7 +42,7 @@ import { |
| 41 | type EnvironmentId, | 42 | type EnvironmentId, |
| 42 | resolveEnvironmentMachineKind, | 43 | resolveEnvironmentMachineKind, |
| 43 | } from "@t3tools/contracts"; | 44 | } from "@t3tools/contracts"; |
| 44 | import { connectionStatusText } from "@t3tools/client-runtime/connection"; | 45 | import { connectionStatusText, connectionStatusTitle } from "@t3tools/client-runtime/connection"; |
| 45 | import { | 46 | import { |
| 46 | isAtomCommandInterrupted, | 47 | isAtomCommandInterrupted, |
| 47 | squashAtomCommandFailure, | 48 | squashAtomCommandFailure, |
| ... | @@ -67,7 +68,7 @@ import { | ... | @@ -67,7 +68,7 @@ import { |
| 67 | } from "./settingsLayout"; | 68 | } from "./settingsLayout"; |
| 68 | import { searchableSetting } from "./settingsSearch"; | 69 | import { searchableSetting } from "./settingsSearch"; |
| 69 | import { EnvironmentIconPicker } from "./EnvironmentIconPicker"; | 70 | import { EnvironmentIconPicker } from "./EnvironmentIconPicker"; |
| 70 | import { LoadBalancingSettings } from "./LoadBalancingSettings"; | 71 | import { LoadBalancingPreference, LoadBalancingSettings } from "./LoadBalancingSettings"; |
| 71 | import { GitHubRoutingSettings } from "./GitHubRoutingSettings"; | 72 | import { GitHubRoutingSettings } from "./GitHubRoutingSettings"; |
| 72 | import { Input } from "../ui/input"; | 73 | import { Input } from "../ui/input"; |
| 73 | import { CommandShortcut } from "../ui/command"; | 74 | import { CommandShortcut } from "../ui/command"; |
| ... | @@ -157,7 +158,7 @@ import { | ... | @@ -157,7 +158,7 @@ import { |
| 157 | import { requestConfirmDialog } from "~/confirmDialog"; | 158 | import { requestConfirmDialog } from "~/confirmDialog"; |
| 158 | import { useAtomCommand } from "../../state/use-atom-command"; | 159 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 159 | import { primaryServerKeybindingsAtom, serverEnvironment } from "~/state/server"; | 160 | import { primaryServerKeybindingsAtom, serverEnvironment } from "~/state/server"; |
| 160 | import { ConnectionStatusDot } from "../ConnectionStatusDot"; | 161 | import { ConnectionStatusDot, connectionPhaseDotClassName } from "../ConnectionStatusDot"; |
| 161 | import { | 162 | import { |
| 162 | ServerUpdateAction, | 163 | ServerUpdateAction, |
| 163 | ServerUpdateProgress, | 164 | ServerUpdateProgress, |
| ... | @@ -1498,37 +1499,40 @@ function SavedBackendListRow({ | ... | @@ -1498,37 +1499,40 @@ function SavedBackendListRow({ |
| 1498 | <div className={cn(ITEM_ROW_CLASSNAME, !enabled && "opacity-60")}> | 1499 | <div className={cn(ITEM_ROW_CLASSNAME, !enabled && "opacity-60")}> |
| 1499 | <div className={ITEM_ROW_INNER_CLASSNAME}> | 1500 | <div className={ITEM_ROW_INNER_CLASSNAME}> |
| 1500 | <div className="min-w-0 flex-1 space-y-1"> | 1501 | <div className="min-w-0 flex-1 space-y-1"> |
| 1501 | <div className="flex min-h-5 items-center gap-1.5"> | 1502 | <div className="flex flex-wrap items-center gap-x-3 gap-y-2"> |
| 1502 | <ConnectionStatusDot | 1503 | <div className="flex min-h-5 min-w-0 max-w-full items-center gap-1.5"> |
| 1503 | tooltipText={statusTooltip} | 1504 | <ConnectionStatusDot |
| 1504 | dotClassName={stateDotClassName} | 1505 | tooltipText={statusTooltip} |
| 1505 | pingClassName={ | 1506 | dotClassName={stateDotClassName} |
| 1506 | enabled && (connectionState === "connecting" || connectionState === "reconnecting") | 1507 | pingClassName={ |
| 1507 | ? "bg-warning/60 duration-2000" | 1508 | enabled && |
| 1508 | : null | 1509 | (connectionState === "connecting" || connectionState === "reconnecting") |
| 1509 | } | 1510 | ? "bg-warning/60 duration-2000" |
| 1510 | /> | 1511 | : null |
| 1511 | <EnvironmentMachineIcon | 1512 | } |
| 1512 | aria-hidden | 1513 | /> |
| 1513 | kind={resolveEnvironmentMachineKind(environment.serverConfig)} | 1514 | <EnvironmentMachineIcon |
| 1514 | className="size-3.5 shrink-0 text-muted-foreground" | 1515 | aria-hidden |
| 1515 | /> | 1516 | kind={resolveEnvironmentMachineKind(environment.serverConfig)} |
| 1516 | <h3 className="min-w-0 truncate text-sm font-medium text-foreground"> | 1517 | className="size-3.5 shrink-0 text-muted-foreground" |
| 1517 | {environment.label} | 1518 | /> |
| 1518 | </h3> | 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 | </div> | 1532 | </div> |
| 1520 | {metadataBits.length > 0 ? ( | 1533 | {metadataBits.length > 0 ? ( |
| 1521 | <p className="truncate text-xs text-muted-foreground">{metadataBits.join(" · ")}</p> | 1534 | <p className="truncate text-xs text-muted-foreground">{metadataBits.join(" · ")}</p> |
| 1522 | ) : null} | 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 | {serverUpdateState.status !== "idle" ? ( | 1536 | {serverUpdateState.status !== "idle" ? ( |
| 1533 | <div className="max-w-md"> | 1537 | <div className="max-w-md"> |
| 1534 | <ServerUpdateProgress state={serverUpdateState} /> | 1538 | <ServerUpdateProgress state={serverUpdateState} /> |
| ... | @@ -1586,12 +1590,12 @@ function SavedBackendListRow({ | ... | @@ -1586,12 +1590,12 @@ function SavedBackendListRow({ |
| 1586 | <TooltipTrigger | 1590 | <TooltipTrigger |
| 1587 | render={ | 1591 | render={ |
| 1588 | <Button size="xs" variant="outline" disabled> | 1592 | <Button size="xs" variant="outline" disabled> |
| 1589 | Managed above | 1593 | Managed locally |
| 1590 | </Button> | 1594 | </Button> |
| 1591 | } | 1595 | } |
| 1592 | /> | 1596 | /> |
| 1593 | <TooltipPopup side="top" className="max-w-80 whitespace-pre-wrap leading-tight"> | 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 | </TooltipPopup> | 1599 | </TooltipPopup> |
| 1596 | </Tooltip> | 1600 | </Tooltip> |
| 1597 | ) : ( | 1601 | ) : ( |
| ... | @@ -1813,6 +1817,19 @@ export function ConnectionsSettings() { | ... | @@ -1813,6 +1817,19 @@ export function ConnectionsSettings() { |
| 1813 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); | 1817 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 1814 | const { environments } = useEnvironments(); | 1818 | const { environments } = useEnvironments(); |
| 1815 | const primaryEnvironment = usePrimaryEnvironment(); | 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 | const connectPairing = useAtomCommand(connectPairingAtom, { reportFailure: false }); | 1833 | const connectPairing = useAtomCommand(connectPairingAtom, { reportFailure: false }); |
| 1817 | const connectSshEnvironment = useAtomCommand(connectSshEnvironmentAtom, { | 1834 | const connectSshEnvironment = useAtomCommand(connectSshEnvironmentAtom, { |
| 1818 | reportFailure: false, | 1835 | reportFailure: false, |
| ... | @@ -3235,8 +3252,8 @@ export function ConnectionsSettings() { | ... | @@ -3235,8 +3252,8 @@ export function ConnectionsSettings() { |
| 3235 | /> | 3252 | /> |
| 3236 | ); | 3253 | ); |
| 3237 | | 3254 | |
| 3238 | return ( | 3255 | const primarySettings = ( |
| 3239 | <SettingsPageContainer> | 3256 | <> |
| 3240 | {canManageLocalBackend ? ( | 3257 | {canManageLocalBackend ? ( |
| 3241 | <> | 3258 | <> |
| 3242 | <SettingsSection | 3259 | <SettingsSection |
| ... | @@ -3324,6 +3341,7 @@ export function ConnectionsSettings() { | ... | @@ -3324,6 +3341,7 @@ export function ConnectionsSettings() { |
| 3324 | {isLocalBackendRemotelyReachable ? ( | 3341 | {isLocalBackendRemotelyReachable ? ( |
| 3325 | <SettingsSection | 3342 | <SettingsSection |
| 3326 | title="Authorized clients" | 3343 | title="Authorized clients" |
| | 3344 | className="[&>div:first-child]:flex-wrap" |
| 3327 | headerAction={ | 3345 | headerAction={ |
| 3328 | <AuthorizedClientsHeaderAction | 3346 | <AuthorizedClientsHeaderAction |
| 3329 | onPairingLinkCreated={handlePairingLinkCreated} | 3347 | onPairingLinkCreated={handlePairingLinkCreated} |
| ... | @@ -3626,9 +3644,15 @@ export function ConnectionsSettings() { | ... | @@ -3626,9 +3644,15 @@ export function ConnectionsSettings() { |
| 3626 | <CloudLinkRow canManageRelay={canManageRelay} /> | 3644 | <CloudLinkRow canManageRelay={canManageRelay} /> |
| 3627 | </SettingsSection> | 3645 | </SettingsSection> |
| 3628 | )} | 3646 | )} |
| | 3647 | </> |
| | 3648 | ); |
| 3629 | | 3649 | |
| | 3650 | return ( |
| | 3651 | <SettingsPageContainer width="wide"> |
| 3630 | <SettingsSection | 3652 | <SettingsSection |
| 3631 | {...searchableSetting("remote-environments")} | 3653 | {...searchableSetting("remote-environments")} |
| | 3654 | title="Environments" |
| | 3655 | variant="plain" |
| 3632 | headerAction={ | 3656 | headerAction={ |
| 3633 | <div className="flex items-center gap-1"> | 3657 | <div className="flex items-center gap-1"> |
| 3634 | {savedServerUpdateTargets.length > 0 ? ( | 3658 | {savedServerUpdateTargets.length > 0 ? ( |
| ... | @@ -3701,21 +3725,117 @@ export function ConnectionsSettings() { | ... | @@ -3701,21 +3725,117 @@ export function ConnectionsSettings() { |
| 3701 | </div> | 3725 | </div> |
| 3702 | } | 3726 | } |
| 3703 | > | 3727 | > |
| 3704 | {savedEnvironments.map((environment) => ( | 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)]"> |
| 3705 | <SavedBackendListRow | 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"> |
| 3706 | key={environment.environmentId} | 3730 | <ScrollArea |
| 3707 | environment={environment} | 3731 | scrollFade |
| 3708 | removingEnvironmentId={removingSavedEnvironmentId} | 3732 | chainVerticalScroll |
| 3709 | onSetEnabled={handleSetSavedBackendEnabled} | 3733 | className="max-h-64 lg:max-h-none lg:min-h-0 lg:flex-1" |
| 3710 | onRemove={handleRemoveSavedBackend} | 3734 | > |
| 3711 | /> | 3735 | <div className="divide-y divide-border/50" aria-label="Environments"> |
| 3712 | ))} | 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 | <CloudRemoteEnvironmentRows | 3834 | <CloudRemoteEnvironmentRows |
| 3714 | primaryEnvironmentId={primaryEnvironmentId} | 3835 | primaryEnvironmentId={primaryEnvironmentId} |
| 3715 | savedEnvironments={savedEnvironments} | 3836 | savedEnvironments={savedEnvironments} |
| 3716 | /> | 3837 | /> |
| 3717 | </SettingsSection> | 3838 | </SettingsSection> |
| 3718 | <GitHubRoutingSettings environments={environments} /> | | |
| 3719 | <LoadBalancingSettings environments={loadBalancingEnvironments} /> | 3839 | <LoadBalancingSettings environments={loadBalancingEnvironments} /> |
| 3720 | </SettingsPageContainer> | 3840 | </SettingsPageContainer> |
| 3721 | ); | 3841 | ); |