Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/desktop/src/lib/trpc/routers/auto-update/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { observable } from "@trpc/server/observable";
import {
type AutoUpdateStatusEvent,
autoUpdateEmitter,
dismissUpdate,
getUpdateStatus,
installUpdate,
simulateDownloading,
simulateError,
simulateUpdateReady,
} from "main/lib/auto-updater";
import { publicProcedure, router } from "../..";

export const createAutoUpdateRouter = () => {
return router({
subscribe: publicProcedure.subscription(() => {
return observable<AutoUpdateStatusEvent>((emit) => {
emit.next(getUpdateStatus());

const onStatusChanged = (event: AutoUpdateStatusEvent) => {
emit.next(event);
};

autoUpdateEmitter.on("status-changed", onStatusChanged);

return () => {
autoUpdateEmitter.off("status-changed", onStatusChanged);
};
});
}),

getStatus: publicProcedure.query(() => {
return getUpdateStatus();
}),

install: publicProcedure.mutation(() => {
installUpdate();
}),

dismiss: publicProcedure.mutation(() => {
dismissUpdate();
}),

simulateReady: publicProcedure.mutation(() => {
simulateUpdateReady();
}),

simulateDownloading: publicProcedure.mutation(() => {
simulateDownloading();
}),

simulateError: publicProcedure.mutation(() => {
simulateError();
}),
});
};
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { BrowserWindow } from "electron";
import { router } from "..";
import { createAuthRouter } from "./auth";
import { createAutoUpdateRouter } from "./auto-update";
import { createChangesRouter } from "./changes";
import { createConfigRouter } from "./config";
import { createExternalRouter } from "./external";
Expand All @@ -26,6 +27,7 @@ import { createWorkspacesRouter } from "./workspaces";
export const createAppRouter = (getWindow: () => BrowserWindow | null) => {
return router({
auth: createAuthRouter(getWindow),
autoUpdate: createAutoUpdateRouter(),
user: createUserRouter(),
window: createWindowRouter(getWindow),
projects: createProjectsRouter(getWindow),
Expand Down
142 changes: 100 additions & 42 deletions apps/desktop/src/main/lib/auto-updater.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,71 @@
import { app, type BrowserWindow, dialog } from "electron";
import { EventEmitter } from "node:events";
import { app, dialog } from "electron";
import { autoUpdater } from "electron-updater";
import { env } from "main/env.main";
import { AUTO_UPDATE_STATUS, type AutoUpdateStatus } from "shared/auto-update";
import { PLATFORM } from "shared/constants";

const UPDATE_CHECK_INTERVAL_MS = 1000 * 60 * 60 * 4; // 4 hours
const UPDATE_FEED_URL =
"https://github.com/superset-sh/superset/releases/latest/download";

let mainWindow: BrowserWindow | null = null;
let isUpdateDialogOpen = false;
export interface AutoUpdateStatusEvent {
status: AutoUpdateStatus;
version?: string;
error?: string;
}

export const autoUpdateEmitter = new EventEmitter();

let currentStatus: AutoUpdateStatus = AUTO_UPDATE_STATUS.IDLE;
let currentVersion: string | undefined;
let isDismissed = false;
Comment on lines +18 to +22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider version-specific dismissal tracking.

The isDismissed boolean flag applies to any READY status, regardless of version. If a user dismisses update v1.0.0 and a newer v1.1.0 is downloaded before the next automatic check (4-hour interval), the notification for the newer version will be suppressed.

While unlikely in production (updates rarely release within a 4-hour window), tracking dismissed versions in a Set<string> would be more robust and ensure users are notified about each distinct version.

🔎 Proposed refactor to track dismissed versions
-let isDismissed = false;
+const dismissedVersions = new Set<string>();

 function emitStatus(
   status: AutoUpdateStatus,
   version?: string,
   error?: string,
 ): void {
   currentStatus = status;
   currentVersion = version;

-  if (isDismissed && status === AUTO_UPDATE_STATUS.READY) {
+  if (version && dismissedVersions.has(version) && status === AUTO_UPDATE_STATUS.READY) {
     return;
   }

   autoUpdateEmitter.emit("status-changed", { status, version, error });
 }

 export function getUpdateStatus(): AutoUpdateStatusEvent {
-  if (isDismissed && currentStatus === AUTO_UPDATE_STATUS.READY) {
+  if (currentVersion && dismissedVersions.has(currentVersion) && currentStatus === AUTO_UPDATE_STATUS.READY) {
     return { status: AUTO_UPDATE_STATUS.IDLE };
   }
   return { status: currentStatus, version: currentVersion };
 }

 export function dismissUpdate(): void {
-  isDismissed = true;
+  if (currentVersion) {
+    dismissedVersions.add(currentVersion);
+  }
   autoUpdateEmitter.emit("status-changed", { status: AUTO_UPDATE_STATUS.IDLE });
 }

Then remove the isDismissed = false reset lines from checkForUpdates (line 59), checkForUpdatesInteractive (line 85), and simulation functions (lines 117, 123).

Committable suggestion skipped: line range outside the PR's diff.


function emitStatus(
status: AutoUpdateStatus,
version?: string,
error?: string,
): void {
currentStatus = status;
currentVersion = version;

if (isDismissed && status === AUTO_UPDATE_STATUS.READY) {
return;
}

autoUpdateEmitter.emit("status-changed", { status, version, error });
}

export function getUpdateStatus(): AutoUpdateStatusEvent {
if (isDismissed && currentStatus === AUTO_UPDATE_STATUS.READY) {
return { status: AUTO_UPDATE_STATUS.IDLE };
}
return { status: currentStatus, version: currentVersion };
}

export function installUpdate(): void {
if (env.NODE_ENV === "development") {
console.info("[auto-updater] Install skipped in dev mode");
emitStatus(AUTO_UPDATE_STATUS.IDLE);
return;
}
autoUpdater.quitAndInstall(false, true);
}

export function setMainWindow(window: BrowserWindow): void {
mainWindow = window;
export function dismissUpdate(): void {
isDismissed = true;
autoUpdateEmitter.emit("status-changed", { status: AUTO_UPDATE_STATUS.IDLE });
}

export function checkForUpdates(): void {
if (env.NODE_ENV === "development" || !PLATFORM.IS_MAC) {
return;
}
isDismissed = false;
emitStatus(AUTO_UPDATE_STATUS.CHECKING);
autoUpdater.checkForUpdates().catch((error) => {
console.error("[auto-updater] Failed to check for updates:", error);
emitStatus(AUTO_UPDATE_STATUS.ERROR, undefined, error.message);
});
}

Expand All @@ -41,19 +87,28 @@ export function checkForUpdatesInteractive(): void {
return;
}

isDismissed = false;
emitStatus(AUTO_UPDATE_STATUS.CHECKING);

autoUpdater
.checkForUpdates()
.then((result) => {
if (!result || !result.updateInfo) {
if (
!result?.updateInfo ||
result.updateInfo.version === app.getVersion()
) {
emitStatus(AUTO_UPDATE_STATUS.IDLE);
dialog.showMessageBox({
type: "info",
title: "No Updates",
message: "You are running the latest version.",
message: "You're up to date!",
detail: `Version ${app.getVersion()} is the latest version.`,
});
}
Comment thread
Kitenite marked this conversation as resolved.
})
.catch((error) => {
console.error("[auto-updater] Failed to check for updates:", error);
emitStatus(AUTO_UPDATE_STATUS.ERROR, undefined, error.message);
dialog.showMessageBox({
type: "error",
title: "Update Error",
Expand All @@ -62,6 +117,28 @@ export function checkForUpdatesInteractive(): void {
});
}

export function simulateUpdateReady(): void {
if (env.NODE_ENV !== "development") return;
isDismissed = false;
emitStatus(AUTO_UPDATE_STATUS.READY, "99.0.0-test");
}

export function simulateDownloading(): void {
if (env.NODE_ENV !== "development") return;
isDismissed = false;
emitStatus(AUTO_UPDATE_STATUS.DOWNLOADING, "99.0.0-test");
}

export function simulateError(): void {
if (env.NODE_ENV !== "development") return;
isDismissed = false;
emitStatus(
AUTO_UPDATE_STATUS.ERROR,
undefined,
"Simulated error for testing",
);
}

export function setupAutoUpdater(): void {
if (env.NODE_ENV === "development" || !PLATFORM.IS_MAC) {
return;
Expand All @@ -78,56 +155,37 @@ export function setupAutoUpdater(): void {

autoUpdater.on("error", (error) => {
console.error("[auto-updater] Error during update check:", error);
emitStatus(AUTO_UPDATE_STATUS.ERROR, undefined, error.message);
});

autoUpdater.on("checking-for-update", () => {
console.info("[auto-updater] Checking for updates...");
emitStatus(AUTO_UPDATE_STATUS.CHECKING);
});

autoUpdater.on("update-available", (info) => {
console.info(
`[auto-updater] Update available: ${info.version}. Downloading...`,
);
emitStatus(AUTO_UPDATE_STATUS.DOWNLOADING, info.version);
});

autoUpdater.on("update-not-available", () => {
console.info("[auto-updater] No updates available");
emitStatus(AUTO_UPDATE_STATUS.IDLE);
});

autoUpdater.on("update-downloaded", (info) => {
if (isUpdateDialogOpen) {
console.info("[auto-updater] Update dialog already open, skipping");
return;
}

autoUpdater.on("download-progress", (progress) => {
console.info(
`[auto-updater] Update downloaded (${info.version}). Prompting user to restart.`,
`[auto-updater] Download progress: ${progress.percent.toFixed(1)}%`,
);
});

isUpdateDialogOpen = true;

const dialogOptions = {
type: "info" as const,
buttons: ["Restart Now", "Later"],
defaultId: 0,
cancelId: 1,
title: "Update Ready",
message: `Version ${info.version} is ready to install`,
detail:
"A new version has been downloaded. Restart the application to apply the update.",
};

const showDialog = mainWindow
? dialog.showMessageBox(mainWindow, dialogOptions)
: dialog.showMessageBox(dialogOptions);

showDialog
.then((response) => {
isUpdateDialogOpen = false;
if (response.response === 0) {
autoUpdater.quitAndInstall(false, true);
}
})
.catch((error) => {
isUpdateDialogOpen = false;
console.error("[auto-updater] Failed to show update dialog:", error);
});
autoUpdater.on("update-downloaded", (info) => {
console.info(
`[auto-updater] Update downloaded (${info.version}). Ready to install.`,
);
emitStatus(AUTO_UPDATE_STATUS.READY, info.version);
});

const interval = setInterval(checkForUpdates, UPDATE_CHECK_INTERVAL_MS);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/lib/local-db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function ensureAppHomeDirExists() {
mkdirSync(SUPERSET_HOME_DIR, { recursive: true });
}
ensureAppHomeDirExists();

/**
* Gets the migrations directory path.
*
Expand Down
29 changes: 28 additions & 1 deletion apps/desktop/src/main/lib/menu.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { COMPANY } from "@superset/shared/constants";
import { app, Menu, shell } from "electron";
import { checkForUpdatesInteractive } from "./auto-updater";
import { env } from "main/env.main";
import {
checkForUpdatesInteractive,
simulateDownloading,
simulateError,
simulateUpdateReady,
} from "./auto-updater";
import { menuEmitter } from "./menu-events";

export function createApplicationMenu() {
Expand Down Expand Up @@ -72,6 +78,27 @@ export function createApplicationMenu() {
},
];

// DEV ONLY: Add Dev menu
if (env.NODE_ENV === "development") {
template.push({
label: "Dev",
submenu: [
{
label: "Simulate Update Downloading",
click: () => simulateDownloading(),
},
{
label: "Simulate Update Ready",
click: () => simulateUpdateReady(),
},
{
label: "Simulate Update Error",
click: () => simulateError(),
},
],
});
}

if (process.platform === "darwin") {
template.unshift({
label: app.name,
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/main/windows/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { NOTIFICATION_EVENTS, PORTS } from "shared/constants";
import { createIPCHandler } from "trpc-electron/main";
import { productName } from "~/package.json";
import { appState } from "../lib/app-state";
import { setMainWindow } from "../lib/auto-updater";
import { createApplicationMenu } from "../lib/menu";
import { playNotificationSound } from "../lib/notification-sound";
import {
Expand Down Expand Up @@ -54,7 +53,6 @@ export async function MainWindow() {
},
});

setMainWindow(window);
createApplicationMenu();

currentWindow = window;
Expand Down
Loading
Loading