Skip to content

Commit d8cd3b4

Browse files
gzdunekravicious
andauthored
[v17] Show app updater in UI (#58261)
* Integrate app updater with UI (#57491) * Implement `checkForUpdates`, `download`, `cancelDownload`, `changeManagingCluster` and `quitAndInstall` * Expose new APIs through Electron IPC * Add app updater context to store updater state * Add `AppUpdates` dialog * Switch "Download" button to "Starting Download..." immediately after clicking it * Clear managing cluster on logout * Return correct error in `GetClusterVersions` * Do not prepend `https://` to file URL, DefaultBaseURL already contains it, trim the checksum value before validating it * Only try to download an update if it's available * Remove separate `will-quit` handler * Clarify comment * Invert condition in `preventInstallingOutdatedUpdates` * Add comments * Move `closeAllConnections()` out of try catch * Add specialized functions to serialize/deserialize errors * Add tests * Leave TODO * Do not wait for `maybeRemoveAppUpdatesManagingCluster` in `useClusterLogout` * Hide app updater for .tar.gz which is not supported * Add missing mock (cherry picked from commit 13aa750) * Show update widget in login dialog (#57695) * Add app updates widget to login dialog * Adjust compatibility warning to app updates * Simplify widget error state, do not show full error message * Make the widget take up 100% width both in login modal an in story * Fix LocalWithPasswordless story * Use `alignItems="stretch"` * Use `SpaceProps` instead of redefining `mx` manually * Infer type * Display buttons in the bottom of Alert * Avoid moving content when scrollbar appears * Destructure `Promise.all` result * Add comment --------- Co-authored-by: Rafał Cieślak <[email protected]> (cherry picked from commit c59a4e6) * Simplify auto updates multi-cluster view (#57709) * Simplify auto updates multi-cluster view * Improve labels * Fix tests (cherry picked from commit b574045) * Connect: Force focus after a successful SSO login * Move login functions from ClustersService to useClusterLogin * Remove no longer used GetResourcesParams * Remove clusters/types.ts * Force focus on Connect after successful SSO login * Update SSO prompt after login but before sync * Add test for SSO prompts * Use `Promise.withResolvers` Co-authored-by: Grzegorz Zdunek <[email protected]> * Update wait-for-sync prompt message --------- Co-authored-by: Grzegorz Zdunek <[email protected]> * Hardcode the lowest supported Connect versions that support managed updates (#58348) * Provide the lowest supported app versions for managed updates * Deserialize errors in the UI code, not in preload Otherwise, the error name is lost. * Bump version in tests * Lint * Show exact version in error message * Do not hide UnsupportedVersionError * Fix margin * Make alert take 100% width (cherry picked from commit d7e1352) --------- Co-authored-by: Rafał Cieślak <[email protected]>
1 parent d818cb9 commit d8cd3b4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2194
-676
lines changed

lib/teleterm/autoupdate/service.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (s *Service) GetClusterVersions(ctx context.Context, _ *api.GetClusterVersi
9696
mu.Lock()
9797
unreachableClusters = append(unreachableClusters, &api.UnreachableCluster{
9898
ClusterUri: cluster.URI.String(),
99-
ErrorMessage: err.Error(),
99+
ErrorMessage: pingErr.Error(),
100100
})
101101
mu.Unlock()
102102
return nil
@@ -147,6 +147,7 @@ func (s *Service) GetDownloadBaseUrl(_ context.Context, _ *api.GetDownloadBaseUr
147147
func resolveBaseURL() (string, error) {
148148
envBaseURL := os.Getenv(autoupdate.BaseURLEnvVar)
149149
if envBaseURL != "" {
150+
// TODO(gzdunek): Validate if it's correct URL.
150151
return envBaseURL, nil
151152
}
152153

web/packages/design/src/StepSlider/StepSlider.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export function StepSlider<Flows>(props: Props<Flows>) {
6161
defaultStepIndex = 0,
6262
tDuration = 500,
6363
wrapping = false,
64+
className,
6465
// extraProps are the props required by our step components defined in our flows.
6566
...extraProps
6667
} = props;
@@ -274,7 +275,7 @@ export function StepSlider<Flows>(props: Props<Flows>) {
274275
const transitionRef = keyToNodeRef.current.get(key);
275276

276277
return (
277-
<Box ref={rootRef} style={rootStyle}>
278+
<Box ref={rootRef} style={rootStyle} className={className}>
278279
{preMount && <HiddenBox>{$preContent}</HiddenBox>}
279280
<Wrap className={animationDirectionPrefix} tDuration={tDuration}>
280281
<TransitionGroup component={null}>
@@ -420,6 +421,8 @@ type Props<Flows> =
420421
* one and backwards from the first one to the last one.
421422
*/
422423
wrapping?: boolean;
424+
/** Allows styling of the container element. */
425+
className?: string;
423426
} & ExtraProps // Extra props that are passed to each step component. Each step of each flow needs to accept the same set of extra props.
424427
: any;
425428

web/packages/teleterm/src/mainProcess/fixtures/mocks.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,26 @@ export class MockMainProcessClient implements MainProcessClient {
177177
async selectDirectoryForDesktopSession() {
178178
return '';
179179
}
180+
181+
supportsAppUpdates() {
182+
return true;
183+
}
184+
async changeAppUpdatesManagingCluster() {}
185+
async maybeRemoveAppUpdatesManagingCluster() {}
186+
async checkForAppUpdates() {}
187+
async downloadAppUpdate() {}
188+
async cancelAppUpdateDownload() {}
189+
async quitAndInstallAppUpdate() {}
190+
subscribeToAppUpdateEvents(): {
191+
cleanup: () => void;
192+
} {
193+
return { cleanup: () => undefined };
194+
}
195+
subscribeToOpenAppUpdateDialog(): {
196+
cleanup: () => void;
197+
} {
198+
return { cleanup: () => undefined };
199+
}
180200
}
181201

182202
export const makeRuntimeSettings = (
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Teleport
3+
* Copyright (C) 2025 Gravitational, Inc.
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
export type SerializedError = {
20+
name: string;
21+
message: string;
22+
stack?: string;
23+
cause?: unknown;
24+
};
25+
26+
/** Serializes an Error into a plain object for transport through Electron IPC. */
27+
export function serializeError(error: Error): SerializedError {
28+
return {
29+
name: error.name,
30+
message: error.message,
31+
cause: error.cause,
32+
stack: error.stack,
33+
};
34+
}
35+
36+
/** Deserializes a plain object back into an Error instance. */
37+
export function deserializeError(serialized: SerializedError): Error {
38+
const error = new Error(serialized.message);
39+
error.name = serialized.name;
40+
error.cause = serialized.cause;
41+
error.stack = serialized.stack;
42+
return error;
43+
}

web/packages/teleterm/src/mainProcess/mainProcess.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {
8383
removeAgentDirectory,
8484
type CreateAgentConfigFileArgs,
8585
} from './createAgentConfigFile';
86+
import { serializeError } from './ipcSerializer';
8687
import { ResolveError, resolveNetworkAddress } from './resolveNetworkAddress';
8788
import { terminateWithTimeout } from './terminateWithTimeout';
8889
import { WindowsManager } from './windowsManager';
@@ -161,7 +162,15 @@ export default class MainProcess {
161162
this.appUpdater = new AppUpdater(
162163
makeAppUpdaterStorage(this.appStateFileStorage),
163164
getClusterVersions,
164-
getDownloadBaseUrl
165+
getDownloadBaseUrl,
166+
event => {
167+
if (event.kind === 'error') {
168+
event.error = serializeError(event.error);
169+
}
170+
this.windowsManager
171+
.getWindow()
172+
.webContents.send(RendererIpc.AppUpdateEvent, event);
173+
}
165174
);
166175
}
167176

@@ -180,8 +189,8 @@ export default class MainProcess {
180189

181190
async dispose(): Promise<void> {
182191
this.windowsManager.dispose();
183-
this.appUpdater.dispose();
184192
await Promise.all([
193+
this.appUpdater.dispose(),
185194
// sending usage events on tshd shutdown has 10-seconds timeout
186195
terminateWithTimeout(this.tshdProcess, 10_000, () => {
187196
this.gracefullyKillTshdProcess();
@@ -639,6 +648,46 @@ export default class MainProcess {
639648
}
640649
);
641650

651+
ipcMain.on(MainProcessIpc.SupportsAppUpdates, event => {
652+
event.returnValue = this.appUpdater.supportsUpdates();
653+
});
654+
655+
ipcMain.handle(MainProcessIpc.CheckForAppUpdates, () =>
656+
this.appUpdater.checkForUpdates()
657+
);
658+
659+
ipcMain.handle(
660+
MainProcessIpc.ChangeAppUpdatesManagingCluster,
661+
(
662+
event,
663+
args: {
664+
clusterUri: RootClusterUri | undefined;
665+
}
666+
) => this.appUpdater.changeManagingCluster(args.clusterUri)
667+
);
668+
669+
ipcMain.handle(
670+
MainProcessIpc.MaybeRemoveAppUpdatesManagingCluster,
671+
(
672+
event,
673+
args: {
674+
clusterUri: RootClusterUri;
675+
}
676+
) => this.appUpdater.maybeRemoveManagingCluster(args.clusterUri)
677+
);
678+
679+
ipcMain.handle(MainProcessIpc.DownloadAppUpdate, () =>
680+
this.appUpdater.download()
681+
);
682+
683+
ipcMain.handle(MainProcessIpc.CancelAppUpdateDownload, () =>
684+
this.appUpdater.cancelDownload()
685+
);
686+
687+
ipcMain.handle(MainProcessIpc.QuiteAndInstallAppUpdate, () =>
688+
this.appUpdater.quitAndInstall()
689+
);
690+
642691
subscribeToTerminalContextMenuEvent(this.configService);
643692
subscribeToTabContextMenuEvent(
644693
this.settings.availableShells,
@@ -673,7 +722,28 @@ export default class MainProcess {
673722
};
674723

675724
const macTemplate: MenuItemConstructorOptions[] = [
676-
{ role: 'appMenu' },
725+
{
726+
role: 'appMenu',
727+
submenu: [
728+
{ role: 'about' },
729+
{
730+
label: 'Check for Updates…',
731+
click: () => {
732+
this.windowsManager
733+
.getWindow()
734+
.webContents.send(RendererIpc.OpenAppUpdateDialog);
735+
},
736+
},
737+
{ type: 'separator' },
738+
{ role: 'services' },
739+
{ type: 'separator' },
740+
{ role: 'hide' },
741+
{ role: 'hideOthers' },
742+
{ role: 'unhide' },
743+
{ type: 'separator' },
744+
{ role: 'quit' },
745+
],
746+
},
677747
{ role: 'editMenu' },
678748
viewMenuTemplate,
679749
{

web/packages/teleterm/src/mainProcess/mainProcessClient.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import { ipcRenderer } from 'electron';
2020

2121
import { CreateAgentConfigFileArgs } from 'teleterm/mainProcess/createAgentConfigFile';
22+
import { AppUpdateEvent } from 'teleterm/services/appUpdater';
2223
import { createFileStorageClient } from 'teleterm/services/fileStorage';
2324
import { RootClusterUri } from 'teleterm/ui/uri';
2425

@@ -199,5 +200,54 @@ export default function createMainProcessClient(): MainProcessClient {
199200
args
200201
);
201202
},
203+
supportsAppUpdates() {
204+
return ipcRenderer.sendSync(MainProcessIpc.SupportsAppUpdates);
205+
},
206+
checkForAppUpdates() {
207+
return ipcRenderer.invoke(MainProcessIpc.CheckForAppUpdates);
208+
},
209+
downloadAppUpdate() {
210+
return ipcRenderer.invoke(MainProcessIpc.DownloadAppUpdate);
211+
},
212+
cancelAppUpdateDownload() {
213+
return ipcRenderer.invoke(MainProcessIpc.CancelAppUpdateDownload);
214+
},
215+
quitAndInstallAppUpdate() {
216+
return ipcRenderer.invoke(MainProcessIpc.QuiteAndInstallAppUpdate);
217+
},
218+
changeAppUpdatesManagingCluster(clusterUri) {
219+
return ipcRenderer.invoke(
220+
MainProcessIpc.ChangeAppUpdatesManagingCluster,
221+
{
222+
clusterUri,
223+
}
224+
);
225+
},
226+
maybeRemoveAppUpdatesManagingCluster(clusterUri) {
227+
return ipcRenderer.invoke(
228+
MainProcessIpc.MaybeRemoveAppUpdatesManagingCluster,
229+
{
230+
clusterUri,
231+
}
232+
);
233+
},
234+
subscribeToAppUpdateEvents: listener => {
235+
const ipcListener = (_, updateEvent: AppUpdateEvent) => {
236+
listener(updateEvent);
237+
};
238+
239+
ipcRenderer.addListener(RendererIpc.AppUpdateEvent, ipcListener);
240+
return {
241+
cleanup: () =>
242+
ipcRenderer.removeListener(RendererIpc.AppUpdateEvent, ipcListener),
243+
};
244+
},
245+
subscribeToOpenAppUpdateDialog: listener => {
246+
ipcRenderer.addListener(RendererIpc.OpenAppUpdateDialog, listener);
247+
return {
248+
cleanup: () =>
249+
ipcRenderer.removeListener(RendererIpc.OpenAppUpdateDialog, listener),
250+
};
251+
},
202252
};
203253
}

web/packages/teleterm/src/mainProcess/types.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import { DeepLinkParseResult } from 'teleterm/deepLinks';
2020
import { CreateAgentConfigFileArgs } from 'teleterm/mainProcess/createAgentConfigFile';
21+
import { AppUpdateEvent } from 'teleterm/services/appUpdater';
2122
import { FileStorage } from 'teleterm/services/fileStorage';
2223
import { Document } from 'teleterm/ui/services/workspacesService';
2324
import { RootClusterUri } from 'teleterm/ui/uri';
@@ -146,10 +147,10 @@ export type MainProcessClient = {
146147
* Tells the OS to focus the window. If wait is true, polls periodically for window status and
147148
* resolves when it's focused or after a short timeout.
148149
*
149-
* Most of the time wait shouldn't be used, it's there for use cases where it's important for the
150-
* app to be focused (e.g., the business logic needs to use the clipboard API). Even in that case,
151-
* the logic must handle a scenario where focus wasn't received as focus cannot be guaranteed.
152-
* Any app can steal focus at any time.
150+
* Most of the time wait shouldn't be used. It's for use cases where the app must be focused
151+
* before carrying out the rest of the logic (e.g., the clipboard API requires focus). Even in
152+
* those cases, the logic must handle a scenario where focus wasn't received as focus cannot be
153+
* guaranteed. Any app can steal focus at any time.
153154
*/
154155
forceFocusWindow(
155156
args?: { wait?: false } | { wait: true; signal?: AbortSignal }
@@ -206,6 +207,23 @@ export type MainProcessClient = {
206207
desktopUri: string;
207208
login: string;
208209
}): Promise<string>;
210+
changeAppUpdatesManagingCluster(
211+
clusterUri: RootClusterUri | undefined
212+
): Promise<void>;
213+
maybeRemoveAppUpdatesManagingCluster(
214+
clusterUri: RootClusterUri
215+
): Promise<void>;
216+
supportsAppUpdates(): boolean;
217+
checkForAppUpdates(): Promise<void>;
218+
downloadAppUpdate(): Promise<void>;
219+
cancelAppUpdateDownload(): Promise<void>;
220+
quitAndInstallAppUpdate(): Promise<void>;
221+
subscribeToAppUpdateEvents(listener: (args: AppUpdateEvent) => void): {
222+
cleanup: () => void;
223+
};
224+
subscribeToOpenAppUpdateDialog(listener: () => void): {
225+
cleanup: () => void;
226+
};
209227
};
210228

211229
export type ChildProcessAddresses = {
@@ -311,6 +329,8 @@ export enum RendererIpc {
311329
NativeThemeUpdate = 'renderer-native-theme-update',
312330
ConnectMyComputerAgentUpdate = 'renderer-connect-my-computer-agent-update',
313331
DeepLinkLaunch = 'renderer-deep-link-launch',
332+
OpenAppUpdateDialog = 'renderer-open-app-update-dialog',
333+
AppUpdateEvent = 'renderer-app-update-event',
314334
}
315335

316336
export enum MainProcessIpc {
@@ -322,6 +342,13 @@ export enum MainProcessIpc {
322342
SaveTextToFile = 'main-process-save-text-to-file',
323343
ForceFocusWindow = 'main-process-force-focus-window',
324344
SelectDirectoryForDesktopSession = 'main-process-select-directory-for-desktop-session',
345+
CheckForAppUpdates = 'main-process-check-for-app-updates',
346+
DownloadAppUpdate = 'main-process-download-app-update',
347+
CancelAppUpdateDownload = 'main-process-cancel-app-update-download',
348+
QuiteAndInstallAppUpdate = 'main-process-quit-and-install-app-update',
349+
ChangeAppUpdatesManagingCluster = 'main-process-change-app-updates-managing-cluster',
350+
MaybeRemoveAppUpdatesManagingCluster = 'main-process-maybe-remove-app-updates-managing-cluster',
351+
SupportsAppUpdates = 'main-process-supports-app-updates',
325352
}
326353

327354
export enum WindowsManagerIpc {

0 commit comments

Comments
 (0)