1import {
2 AuthSettingsWriteScope,
3 ENVIRONMENT_MACHINE_KINDS,
4 isEnvironmentMachineKind,
5 resolveEnvironmentMachineKind,
6 type EnvironmentId,
7 type ServerConfig,
8} from "@t3tools/contracts";
9
10import { useUpdateEnvironmentSettings } from "../../hooks/useSettings";
11import { useEnvironmentScope } from "../../state/session";
12import { ENVIRONMENT_MACHINE_KIND_LABELS, EnvironmentMachineIcon } from "../EnvironmentMachineIcon";
13import {
14 MenuItem,
15 MenuRadioGroup,
16 MenuRadioItem,
17 MenuSeparator,
18 MenuSub,
19 MenuSubPopup,
20 MenuSubTrigger,
21} from "../ui/menu";
22
23/**
24 * Why the picker is inert, in the order the user can do something about it.
25 * Null means it can be changed.
26 */
27export function resolveEnvironmentIconPickerLock(input: {
28 readonly serverConfig: ServerConfig | null;
29 readonly operateAccess: "granted" | "denied" | "pending";
30}): string | null {
31 if (input.serverConfig === null) {
32 return "Connect to this environment to change its icon.";
33 }
34 if (input.serverConfig.environment.capabilities.environmentIcon !== true) {
35 return "This environment's server is too old to keep an icon. Update it to choose one.";
36 }
37 if (input.operateAccess !== "granted") {
38 return "Your session on this environment cannot change its settings.";
39 }
40 return null;
41}
42
43/**
44 * "Icon" submenu for an environment's row menu. Lists the machine kinds with
45 * the server's own detection marked, so the user can tell whether detection
46 * got it right before overriding. Picking the detected kind clears the
47 * override. Locked environments show the reason as a disabled item instead of
48 * hiding the submenu, so the current icon still reads.
49 */
50export function EnvironmentIconMenu({
51 environmentId,
52 serverConfig,
53}: {
54 readonly environmentId: EnvironmentId;
55 readonly serverConfig: ServerConfig | null;
56}) {
57 const updateSettings = useUpdateEnvironmentSettings(environmentId);
58 const operateAccess = useEnvironmentScope(environmentId, AuthSettingsWriteScope)
59 ? "granted"
60 : "denied";
61 const lock = resolveEnvironmentIconPickerLock({ serverConfig, operateAccess });
62 // With no detection the server falls back to "server", so picking that
63 // kind clears the override the same way picking the detected kind does.
64 const detected = serverConfig?.environment.platform.machine ?? "server";
65 const resolved = resolveEnvironmentMachineKind(serverConfig);
66
67 return (
68 <MenuSub>
69 <MenuSubTrigger>
70 <EnvironmentMachineIcon kind={resolved} />
71 Icon
72 </MenuSubTrigger>
73 <MenuSubPopup>
74 {lock !== null ? (
75 <>
76 <MenuItem disabled className="whitespace-normal">
77 {lock}
78 </MenuItem>
79 <MenuSeparator />
80 </>
81 ) : null}
82 <MenuRadioGroup
83 value={resolved}
84 onValueChange={(next) => {
85 if (lock !== null || !isEnvironmentMachineKind(next)) return;
86 updateSettings({ environmentIcon: next === detected ? null : next });
87 }}
88 >
89 {ENVIRONMENT_MACHINE_KINDS.map((kind) => (
90 <MenuRadioItem key={kind} value={kind} disabled={lock !== null}>
91 <span className="flex min-w-0 items-center gap-2">
92 <EnvironmentMachineIcon kind={kind} className="size-3.5 shrink-0" />
93 <span className="min-w-0 flex-1 truncate">
94 {ENVIRONMENT_MACHINE_KIND_LABELS[kind]}
95 </span>
96 {kind === detected ? (
97 <span className="shrink-0 text-xs text-muted-foreground">
98 {serverConfig?.environment.platform.machine ? "detected" : "default"}
99 </span>
100 ) : null}
101 </span>
102 </MenuRadioItem>
103 ))}
104 </MenuRadioGroup>
105 </MenuSubPopup>
106 </MenuSub>
107 );
108}