Skip to content
Source
Sign inSign up
Open History to see the last commit

UpdateTrackSwitchDialog.tsx

194 lines7.1 KB

1import { useAtomValue } from "@effect/atom-react";
2import { settlePromise, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
3import type { DesktopUpdateChannel } from "@t3tools/contracts";
4import { acceptsTrackSwitchRelease, desktopUpdateChannelOf } from "@t3tools/shared/cliRelease";
5import { isMacPlatform } from "@t3tools/shared/keybindings";
6import { useState } from "react";
7import { AsyncResult } from "effect/reactivity";
8​
9import { APP_VERSION } from "~/branding";
10import { isLocalEnvironmentDisabled } from "~/localEnvironment";
11import { useDesktopUpdateState } from "~/state/desktopUpdate";
12import { useEnvironments } from "~/state/environments";
13import { npmReleaseAtom, npmReleaseFailureMessage } from "~/state/npmRelease";
14import { downloadLocalUpdate, installLocalUpdate } from "../sidebar/SidebarUpdatePill";
15import { UpdateEverythingDialog } from "../sidebar/UpdateEverythingDialog";
16import {
17 channelSwitchLabel,
18 localSwitchBlockReason,
19 type ChannelSwitchRelease,
20} from "../sidebar/updateEverything.logic";
21import { isHostedByThisApp, useUpdateEverything } from "../sidebar/useUpdateEverything";
22import {
23 localUpdaterStaysPut,
24 nothingToMove,
25 prepareLocalTrackSwitch,
26} from "./updateTrackSwitch.logic";
27import {
28 AlertDialog,
29 AlertDialogClose,
30 AlertDialogDescription,
31 AlertDialogFooter,
32 AlertDialogHeader,
33 AlertDialogPopup,
34 AlertDialogTitle,
35} from "../ui/alert-dialog";
36import { Button } from "../ui/button";
37import { stackedThreadToast, toastManager } from "../ui/toast";
38​
39/** Moves this app and every connected machine to `channel`. */
40export function UpdateTrackSwitchDialog({
41 channel,
42 onClose,
43}: {
44 readonly channel: DesktopUpdateChannel;
45 readonly onClose: () => void;
46}) {
47 // npm dist-tags match the desktop channel names.
48 const release = useAtomValue(npmReleaseAtom(channel));
49 if (AsyncResult.isSuccess(release) && release.value !== null) {
50 return <SwitchPlan release={{ channel, ...release.value }} onClose={onClose} />;
51 }
52 const label = channelSwitchLabel(channel);
53 return (
54 <AlertDialog open onOpenChange={(open) => !open && onClose()}>
55 <AlertDialogPopup>
56 <AlertDialogHeader>
57 <AlertDialogTitle>Switch to {label}</AlertDialogTitle>
58 <AlertDialogDescription>
59 {AsyncResult.isFailure(release)
60 ? npmReleaseFailureMessage(release.cause)
61 : AsyncResult.isSuccess(release)
62 ? `Could not find the latest ${label} release in this environment's npm registry.`
63 : "Checking releases…"}
64 </AlertDialogDescription>
65 </AlertDialogHeader>
66 <AlertDialogFooter>
67 <AlertDialogClose render={<Button variant="outline" />}>Cancel</AlertDialogClose>
68 </AlertDialogFooter>
69 </AlertDialogPopup>
70 </AlertDialog>
71 );
73​
74function SwitchPlan({
75 release,
76 onClose,
77}: {
78 readonly release: ChannelSwitchRelease;
79 readonly onClose: () => void;
80}) {
81 const updateState = useDesktopUpdateState();
82 const { environments } = useEnvironments();
83 const crossesLocal = desktopUpdateChannelOf(APP_VERSION) !== release.channel;
84 const updatesOff = localUpdaterStaysPut(updateState, release.channel);
85 // After a partial switch this app may already be on the track; then only remotes move.
86 const switchesLocal = crossesLocal && !updatesOff;
87 const [checking, setChecking] = useState(false);
88 const everything = useUpdateEverything(switchesLocal ? release.version : null, release);
89 const label = channelSwitchLabel(release.channel);
90 const idle = nothingToMove({
91 label,
92 channel: release.channel,
93 savedChannel: updateState?.channel ?? null,
94 switchesLocal,
95 crossesLocal,
96 rows: everything.rows,
97 hasTargets: everything.hasUpdates,
98 });
99 // This app's reinstall restarts every server it hosts, so each of their databases must allow it.
100 const localBlocked = localSwitchBlockReason(
101 APP_VERSION,
102 release,
103 environments.filter(isHostedByThisApp).map((environment) => ({
104 connected: environment.connection.phase === "connected",
105 databaseMigration: environment.serverConfig?.environment.databaseMigration,
106 })),
107 window.desktopBridge?.getLocalEnvironmentBootstraps().length ?? 0,
108 !isLocalEnvironmentDisabled(),
109 );
110​
111 const run = async () => {
112 const bridge = window.desktopBridge;
113 if (!bridge) return;
114 // Stay open while the feed is checked; the run's own progress takes over after.
115 setChecking(true);
116 const fail = (description: string) =>
117 toastManager.add(
118 stackedThreadToast({ type: "error", title: `Could not switch to ${label}`, description }),
119 );
120 const prepared = await prepareLocalTrackSwitch(bridge, {
121 channel: release.channel,
122 version: release.version,
123 reinstall: switchesLocal,
124 });
125 onClose();
126 if (!prepared.ok) {
127 fail(prepared.message);
128 return;
129 }
130 const { local } = prepared;
131 const result = await settlePromise(() =>
132 everything.run(
133 local === null
134 ? {}
135 : {
136 ...(local.status === "available"
137 ? {
138 downloadLocal: (reportError) =>
139 downloadLocalUpdate(bridge, { notifyDownloaded: false, reportError }),
140 }
141 : {}),
142 // Install only a build the database check covers, pinned so nothing replaces it.
143 installLocal: async (reportError) => {
144 const { downloadedVersion } = await bridge.getUpdateState();
145 if (
146 downloadedVersion === null ||
147 !acceptsTrackSwitchRelease(downloadedVersion, release.channel, release.version)
148 ) {
149 reportError(
150 "Could not install update",
151 `This app downloaded ${downloadedVersion ?? "nothing"}, not ${release.version} or newer.`,
152 );
153 return false;
154 }
155 return installLocalUpdate(bridge, reportError, downloadedVersion);
156 },
157 },
158 ),
159 );
160 if (result._tag === "Failure") {
161 const error = squashAtomCommandFailure(result);
162 fail(error instanceof Error ? error.message : "An unexpected error occurred.");
163 }
164 };
165​
166 return (
167 <UpdateEverythingDialog
168 open
169 onOpenChange={(open) => !open && !checking && onClose()}
170 title={`Switch to ${label}?`}
171 version={release.version}
172 providerCount={0}
173 rows={everything.rows}
174 notes={[
175 ...(idle.note === null ? [] : [idle.note]),
176 ...(crossesLocal && updatesOff
177 ? ["This app's updates are off here, so reinstall it yourself to switch."]
178 : []),
179 ...(switchesLocal && isMacPlatform(navigator.platform)
180 ? ["If T3 Code does not reopen after it restarts, open it again."]
181 : []),
182 ]}
183 restarts={everything.restarts}
184 confirmLabel={checking ? "Checking…" : `Switch to ${label}`}
185 busy={checking}
186 disabledReason={
187 switchesLocal && localBlocked !== null
188 ? `This app cannot switch: ${localBlocked}.`
189 : idle.disabledReason
190 }
191 onConfirm={() => void run()}
192 />
193 );