Add support for automatic updates in the pgAdmin 4 Desktop application on macOS. #5766
parent
6db0cc5c5d
commit
9eec4f5b8c
3
Makefile
3
Makefile
|
|
@ -20,8 +20,9 @@ APP_REVISION := $(shell grep ^APP_REVISION web/version.py | awk -F"=" '{print $$
|
|||
# Include only platform-independent builds in all
|
||||
all: docs pip src
|
||||
|
||||
# Add BUILD_OPTS variable to pass arguments
|
||||
appbundle:
|
||||
./pkg/mac/build.sh
|
||||
./pkg/mac/build.sh $(BUILD_OPTS)
|
||||
|
||||
install-node:
|
||||
cd web && yarn install
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,96 @@
|
|||
Auto-Update of pgAdmin 4 Desktop Application
|
||||
********************************************
|
||||
|
||||
pgAdmin 4's desktop application includes an automated update system built using
|
||||
Electron's ``autoUpdater`` module. This feature enables users to receive and install
|
||||
updates seamlessly, ensuring they always have access to the latest features and security fixes.
|
||||
|
||||
Supported Platforms
|
||||
===================
|
||||
|
||||
- **macOS:** Fully supported with automatic updates enabled by default
|
||||
- **Windows:** Not supported
|
||||
- **Linux:** Not supported
|
||||
|
||||
Update Process Overview
|
||||
=======================
|
||||
|
||||
1. **Check for Updates:**
|
||||
|
||||
- Automatic check on application startup
|
||||
- Manual check available via pgAdmin 4 menu > Check for Updates
|
||||
- Uses Electron's ``autoUpdater`` API to query update server
|
||||
|
||||
2. **Download Process:**
|
||||
|
||||
- Updates download automatically when detected
|
||||
- Progress shown via notifications
|
||||
- Background download prevents interruption of work
|
||||
|
||||
3. **Installation Flow:**
|
||||
|
||||
- User prompted to Install & Restart or Restart Later when update ready
|
||||
- Update applied during application restart
|
||||
|
||||
The flow chart for the update process is as follows:
|
||||
|
||||
.. image:: images/auto_update_desktop_app.png
|
||||
:alt: Auto-update Desktop App
|
||||
:align: center
|
||||
|
||||
User Interface Components
|
||||
=========================
|
||||
|
||||
1. **Notification Types:**
|
||||
|
||||
- Update available
|
||||
- Download progress
|
||||
- Update ready to install
|
||||
- Error notifications
|
||||
|
||||
2. **Menu Integration:**
|
||||
|
||||
- Check for Updates option in pgAdmin 4 menu
|
||||
- Restart to Update option when update available
|
||||
|
||||
Error Handling
|
||||
==============
|
||||
|
||||
The system includes comprehensive error handling:
|
||||
|
||||
1. **Network Errors:**
|
||||
|
||||
- Connection timeouts
|
||||
- Download failures
|
||||
- Server unavailability
|
||||
|
||||
2. **Installation Errors:**
|
||||
|
||||
- Corrupted downloads
|
||||
|
||||
3. **Recovery Mechanisms:**
|
||||
|
||||
- Fallback to manual update
|
||||
- Error reporting to logs
|
||||
|
||||
Security Considerations
|
||||
=======================
|
||||
|
||||
The update system implements below security measures:
|
||||
|
||||
1. **Secure Communication:**
|
||||
|
||||
- Protected update metadata
|
||||
|
||||
Platform-Specific Notes
|
||||
=======================
|
||||
|
||||
1. **macOS:**
|
||||
|
||||
- Uses native update mechanisms
|
||||
- Requires signed packages
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
- `Electron autoUpdater API Documentation <https://www.electronjs.org/docs/latest/api/auto-updater>`_
|
||||
|
|
@ -128,3 +128,10 @@ The configuration settings are stored in *runtime_config.json* file, which
|
|||
will be available on Unix systems (~/.local/share/pgadmin/),
|
||||
on Mac OS X (~/Library/Preferences/pgadmin),
|
||||
and on Windows (%APPDATA%/pgadmin).
|
||||
|
||||
For details on the auto-update system for the desktop application, see
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
auto_update_desktop_app
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
|
|
@ -31,11 +31,15 @@ Either build the sources or get them from macports or similar:
|
|||
*notarization.conf* and set the values accordingly. Note that notarization
|
||||
will fail if the code isn't signed.
|
||||
|
||||
4. To build, go to pgAdmin4 source root directory and execute:
|
||||
4. To build only DMG file, go to pgAdmin4 source root directory and execute:
|
||||
|
||||
make appbundle
|
||||
|
||||
To build both DMG and ZIP files, go to pgAdmin4 source root directory and execute:
|
||||
|
||||
make appbundle BUILD_OPTS="--zip"
|
||||
|
||||
This will create the python virtual environment and install all the required
|
||||
python modules mentioned in the requirements file using pip, build the
|
||||
runtime code and finally create the app bundle and the DMG in *./dist*
|
||||
runtime code and finally create the app bundle and the DMG and/or ZIP in *./dist*
|
||||
directory.
|
||||
|
|
|
|||
|
|
@ -385,6 +385,24 @@ _codesign_bundle() {
|
|||
-i org.pgadmin.pgadmin4 \
|
||||
--sign "${DEVELOPER_ID}" \
|
||||
"${BUNDLE_DIR}"
|
||||
|
||||
echo "Verifying the signature from bundle dir..."
|
||||
codesign --verify --deep --verbose=4 "${BUNDLE_DIR}"
|
||||
}
|
||||
|
||||
_create_zip() {
|
||||
ZIP_NAME="${DMG_NAME%.dmg}.zip"
|
||||
echo "ZIP_NAME: ${ZIP_NAME}"
|
||||
|
||||
echo "Compressing pgAdmin 4.app in bundle dir into ${ZIP_NAME}..."
|
||||
ditto -c -k --sequesterRsrc --keepParent "${BUNDLE_DIR}" "${ZIP_NAME}"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to create the ZIP file. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Successfully created ZIP file: ${ZIP_NAME}"
|
||||
}
|
||||
|
||||
_create_dmg() {
|
||||
|
|
@ -426,18 +444,23 @@ _codesign_dmg() {
|
|||
"${DMG_NAME}"
|
||||
}
|
||||
|
||||
|
||||
_notarize_pkg() {
|
||||
local FILE_NAME="$1"
|
||||
local STAPLE_TARGET="$2"
|
||||
local FILE_LABEL="$3"
|
||||
|
||||
if [ "${CODESIGN}" -eq 0 ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Uploading DMG for Notarization ..."
|
||||
STATUS=$(xcrun notarytool submit "${DMG_NAME}" \
|
||||
echo "Uploading ${FILE_LABEL} for Notarization ..."
|
||||
STATUS=$(xcrun notarytool submit "${FILE_NAME}" \
|
||||
--team-id "${DEVELOPER_TEAM_ID}" \
|
||||
--apple-id "${DEVELOPER_USER}" \
|
||||
--password "${DEVELOPER_ASP}" 2>&1)
|
||||
|
||||
echo "${STATUS}"
|
||||
|
||||
# Get the submission ID
|
||||
SUBMISSION_ID=$(echo "${STATUS}" | awk -F ': ' '/id:/ { print $2; exit; }')
|
||||
echo "Notarization submission ID: ${SUBMISSION_ID}"
|
||||
|
|
@ -461,11 +484,28 @@ _notarize_pkg() {
|
|||
fi
|
||||
|
||||
# Staple the notarization
|
||||
echo "Stapling the notarization to the pgAdmin DMG..."
|
||||
if ! xcrun stapler staple "${DMG_NAME}"; then
|
||||
echo "Stapling the notarization to the ${FILE_LABEL}..."
|
||||
if ! xcrun stapler staple "${STAPLE_TARGET}"; then
|
||||
echo "Stapling failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# For ZIP, recreate the zip after stapling
|
||||
if [[ "${FILE_LABEL}" == "ZIP" ]]; then
|
||||
ditto -c -k --keepParent "${BUNDLE_DIR}" "${ZIP_NAME}"
|
||||
if [ $? != 0 ]; then
|
||||
echo "ERROR: could not staple ${ZIP_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Notarization completed successfully."
|
||||
}
|
||||
|
||||
_notarize_zip() {
|
||||
_notarize_pkg "${ZIP_NAME}" "${BUNDLE_DIR}" "ZIP"
|
||||
}
|
||||
|
||||
_notarize_dmg() {
|
||||
_notarize_pkg "${DMG_NAME}" "${DMG_NAME}" "DMG"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,32 @@ if [ "${PGADMIN_PYTHON_VERSION}" == "" ]; then
|
|||
export PGADMIN_PYTHON_VERSION=3.13.1
|
||||
fi
|
||||
|
||||
# Initialize variables
|
||||
CREATE_ZIP=0
|
||||
CREATE_DMG=1
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--zip)
|
||||
CREATE_ZIP=1
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo "Options:"
|
||||
echo " --zip Create both ZIP and DMG files"
|
||||
echo " --help Display this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/build-functions.sh"
|
||||
|
||||
|
|
@ -69,6 +95,16 @@ _complete_bundle
|
|||
_generate_sbom
|
||||
_codesign_binaries
|
||||
_codesign_bundle
|
||||
|
||||
# Handle ZIP creation if requested
|
||||
if [ "${CREATE_ZIP}" -eq 1 ]; then
|
||||
_create_zip
|
||||
_notarize_zip
|
||||
fi
|
||||
|
||||
# Handle DMG creation if not disabled
|
||||
if [ "${CREATE_DMG}" -eq 1 ]; then
|
||||
_create_dmg
|
||||
_codesign_dmg
|
||||
_notarize_pkg
|
||||
_notarize_dmg
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
import { autoUpdater, ipcMain } from 'electron';
|
||||
import { refreshMenus } from './menu.js';
|
||||
import * as misc from './misc.js';
|
||||
|
||||
// This function stores the flags in configStore that are needed
|
||||
// for auto-update and refreshes menus
|
||||
export function updateConfigAndMenus(event, configStore, pgAdminMainScreen, menuCallbacks) {
|
||||
const flags = {
|
||||
'update-available': { update_downloading: true },
|
||||
'update-not-available': { update_downloading: false },
|
||||
'update-downloaded': { update_downloading: false, update_downloaded: true },
|
||||
'error-close': { update_downloading: false, update_downloaded: false },
|
||||
};
|
||||
const flag = flags[event];
|
||||
if (flag) {
|
||||
Object.entries(flag).forEach(([k, v]) => configStore.set(k, v));
|
||||
refreshMenus(pgAdminMainScreen, configStore, menuCallbacks);
|
||||
}
|
||||
}
|
||||
|
||||
// This function registers autoUpdater event listeners ONCE
|
||||
function registerAutoUpdaterEvents({ pgAdminMainScreen, configStore, menuCallbacks }) {
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
misc.writeServerLog('[Auto-Updater]: Checking for update...');
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
updateConfigAndMenus('update-available', configStore, pgAdminMainScreen, menuCallbacks);
|
||||
misc.writeServerLog('[Auto-Updater]: Update downloading...');
|
||||
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', { update_downloading: true });
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
updateConfigAndMenus('update-not-available', configStore, pgAdminMainScreen, menuCallbacks);
|
||||
misc.writeServerLog('[Auto-Updater]: No update available...');
|
||||
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', { no_update_available: true });
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
updateConfigAndMenus('update-downloaded', configStore, pgAdminMainScreen, menuCallbacks);
|
||||
misc.writeServerLog('[Auto-Updater]: Update downloaded...');
|
||||
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', { update_downloaded: true });
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (message) => {
|
||||
updateConfigAndMenus('error-close', configStore, pgAdminMainScreen, menuCallbacks);
|
||||
misc.writeServerLog(`[Auto-Updater]: ${message}`);
|
||||
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', { error: true, errMsg: message });
|
||||
});
|
||||
}
|
||||
|
||||
// Handles 'sendDataForAppUpdate' IPC event: updates config, refreshes menus, triggers update check, or installs update if requested.
|
||||
function handleSendDataForAppUpdate({
|
||||
pgAdminMainScreen,
|
||||
configStore,
|
||||
menuCallbacks,
|
||||
baseUrl,
|
||||
UUID,
|
||||
forceQuitAndInstallUpdate,
|
||||
}) {
|
||||
return (_, data) => {
|
||||
// Only update the auto-update enabled flag and refresh menus if the value has changed or is not set
|
||||
if (typeof data.check_for_updates !== 'undefined') {
|
||||
const currentFlag = configStore.get('auto_update_enabled');
|
||||
if (typeof currentFlag === 'undefined' || currentFlag !== data.check_for_updates) {
|
||||
configStore.set('auto_update_enabled', data.check_for_updates);
|
||||
refreshMenus(pgAdminMainScreen, configStore, menuCallbacks);
|
||||
}
|
||||
}
|
||||
// If auto-update is enabled, proceed with the update check
|
||||
if (
|
||||
data.auto_update_url &&
|
||||
data.upgrade_version &&
|
||||
data.upgrade_version_int &&
|
||||
data.current_version_int &&
|
||||
data.product_name
|
||||
) {
|
||||
const ftpUrl = encodeURIComponent(
|
||||
`${data.auto_update_url}/pgadmin4-${data.upgrade_version}-${process.arch}.zip`
|
||||
);
|
||||
let serverUrl = `${baseUrl}/misc/auto_update/${data.current_version_int}/${data.upgrade_version}/${data.upgrade_version_int}/${data.product_name}/${ftpUrl}/?key=${UUID}`;
|
||||
|
||||
try {
|
||||
autoUpdater.setFeedURL({ url: serverUrl });
|
||||
misc.writeServerLog('[Auto-Updater]: Initiating update check...');
|
||||
autoUpdater.checkForUpdates();
|
||||
} catch (err) {
|
||||
misc.writeServerLog('[Auto-Updater]: Error setting autoUpdater feed URL: ' + err.message);
|
||||
if (pgAdminMainScreen) {
|
||||
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', {
|
||||
error: true,
|
||||
errMsg: 'Failed to check for updates. Please try again later.',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If the user has requested to install the update immediately
|
||||
if (data.install_update_now) {
|
||||
forceQuitAndInstallUpdate();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function setupAutoUpdater({
|
||||
pgAdminMainScreen,
|
||||
configStore,
|
||||
menuCallbacks,
|
||||
baseUrl,
|
||||
UUID,
|
||||
forceQuitAndInstallUpdate,
|
||||
}) {
|
||||
// For now only macOS is supported for electron auto-update
|
||||
if (process.platform === 'darwin') {
|
||||
registerAutoUpdaterEvents({ pgAdminMainScreen, configStore, menuCallbacks });
|
||||
ipcMain.on(
|
||||
'sendDataForAppUpdate',
|
||||
handleSendDataForAppUpdate({
|
||||
pgAdminMainScreen,
|
||||
configStore,
|
||||
menuCallbacks,
|
||||
baseUrl,
|
||||
UUID,
|
||||
forceQuitAndInstallUpdate,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { app, Menu, ipcMain, BrowserWindow, globalShortcut } from 'electron';
|
|||
const isMac = process.platform == 'darwin';
|
||||
const isLinux = process.platform == 'linux';
|
||||
let mainMenu;
|
||||
let cachedMenus;
|
||||
|
||||
// Use to convert shortcut to accelerator for electron.
|
||||
function convertShortcutToAccelerator({ control, meta, shift, alt, key } = {}) {
|
||||
|
|
@ -29,12 +30,9 @@ function convertShortcutToAccelerator({ control, meta, shift, alt, key } = {}) {
|
|||
return [...mods, k].join('+');
|
||||
}
|
||||
|
||||
function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
||||
const template = [];
|
||||
|
||||
// bind all menus click event.
|
||||
pgadminMenus = pgadminMenus.map((menuItem)=>{
|
||||
return {
|
||||
// Binds click events to all menu and submenu items recursively.
|
||||
function bindMenuClicks(pgadminMenus, pgAdminMainScreen) {
|
||||
return pgadminMenus.map((menuItem) => ({
|
||||
...menuItem,
|
||||
submenu: menuItem.submenu?.map((subMenuItem) => {
|
||||
const smName = `${menuItem.name}_${subMenuItem.name}`;
|
||||
|
|
@ -50,8 +48,7 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
|||
}
|
||||
pgAdminMainScreen.webContents.send('menu-click', smName);
|
||||
},
|
||||
submenu: subMenuItem.submenu?.map((deeperSubMenuItem)=>{
|
||||
return {
|
||||
submenu: subMenuItem.submenu?.map((deeperSubMenuItem) => ({
|
||||
...deeperSubMenuItem,
|
||||
accelerator: convertShortcutToAccelerator(deeperSubMenuItem.shortcut),
|
||||
click: (_menuItem, _browserWindow, event)=>{
|
||||
|
|
@ -63,26 +60,69 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
|||
}
|
||||
pgAdminMainScreen.webContents.send('menu-click', `${smName}_${deeperSubMenuItem.name}`);
|
||||
},
|
||||
})),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
// Handles auto-update related menu items for macOS.
|
||||
// Adds or disables update menu items based on config state.
|
||||
function handleAutoUpdateMenu(menuFile, configStore, callbacks) {
|
||||
if (!configStore.get('auto_update_enabled')) return;
|
||||
if (configStore.get('update_downloaded')) {
|
||||
// Add "Restart to Update" if update is downloaded
|
||||
menuFile.submenu.unshift({
|
||||
name: 'mnu_restart_to_update',
|
||||
id: 'mnu_restart_to_update',
|
||||
label: 'Restart to Update...',
|
||||
enabled: true,
|
||||
priority: 998,
|
||||
click: callbacks['restart_to_update'],
|
||||
});
|
||||
} else {
|
||||
// Add "Check for Updates" if update is not downloaded
|
||||
menuFile.submenu.unshift({
|
||||
name: 'mnu_check_updates',
|
||||
id: 'mnu_check_updates',
|
||||
label: 'Check for Updates...',
|
||||
enabled: true,
|
||||
priority: 998,
|
||||
click: callbacks['check_for_updates'],
|
||||
});
|
||||
}
|
||||
// Disable "Check for Updates" if update is downloading
|
||||
if (configStore.get('update_downloading')) {
|
||||
menuFile.submenu.forEach((item) => {
|
||||
if (item.id == 'mnu_check_updates') item.enabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove About pgAdmin 4 from help menu and add it to the top of menuFile submenu.
|
||||
function moveAboutMenuToTop(pgadminMenus, menuFile) {
|
||||
const helpMenu = pgadminMenus.find((menu) => menu.name == 'help');
|
||||
if (!helpMenu) return;
|
||||
const aboutItem = helpMenu.submenu.find((item) => item.name === 'mnu_about');
|
||||
if (!aboutItem) return;
|
||||
helpMenu.submenu = helpMenu.submenu.filter((item) => item.name !== 'mnu_about');
|
||||
menuFile.submenu.unshift(aboutItem);
|
||||
menuFile.submenu.splice(2, 0, { type: 'separator' });
|
||||
}
|
||||
|
||||
// Builds the application menu template and binds menu click events.
|
||||
// Handles platform-specific menu structure and dynamic menu items.
|
||||
function buildMenu(pgadminMenus, pgAdminMainScreen, configStore, callbacks) {
|
||||
const template = [];
|
||||
|
||||
pgadminMenus = bindMenuClicks(pgadminMenus, pgAdminMainScreen);
|
||||
|
||||
let menuFile = pgadminMenus.shift();
|
||||
|
||||
// macOS-specific menu modifications
|
||||
if (isMac) {
|
||||
// Remove About pgAdmin 4 from help menu and add it to the top of menuFile submenu.
|
||||
const helpMenu = pgadminMenus.find((menu) => menu.name == 'help');
|
||||
if (helpMenu) {
|
||||
const aboutItem = helpMenu.submenu.find((item) => item.name === 'mnu_about');
|
||||
if (aboutItem) {
|
||||
helpMenu.submenu = helpMenu.submenu.filter((item) => item.name !== 'mnu_about');
|
||||
menuFile.submenu.unshift(aboutItem);
|
||||
menuFile.submenu.splice(1, 0, { type: 'separator' });
|
||||
}
|
||||
}
|
||||
handleAutoUpdateMenu(menuFile, configStore, callbacks);
|
||||
moveAboutMenuToTop(pgadminMenus, menuFile);
|
||||
}
|
||||
|
||||
template.push({
|
||||
|
|
@ -90,19 +130,17 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
|||
submenu: [
|
||||
...menuFile.submenu,
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'View Logs...', click: callbacks['view_logs'],
|
||||
},
|
||||
{
|
||||
label: 'Configure runtime...', click: callbacks['configure'],
|
||||
},
|
||||
{ label: 'View Logs...', click: callbacks['view_logs'] },
|
||||
{ label: 'Configure runtime...', click: callbacks['configure'] },
|
||||
{ type: 'separator' },
|
||||
...(isMac ? [
|
||||
...(isMac
|
||||
? [
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
] : []),
|
||||
]
|
||||
: []),
|
||||
{ role: 'quit' },
|
||||
],
|
||||
});
|
||||
|
|
@ -120,7 +158,13 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
|||
label: 'View',
|
||||
submenu: [
|
||||
{ label: 'Reload', click: callbacks['reloadApp'] },
|
||||
{ label: 'Toggle Developer Tools', click: ()=>BrowserWindow.getFocusedWindow().webContents.openDevTools({ mode: 'bottom' })},
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
click: () =>
|
||||
BrowserWindow.getFocusedWindow().webContents.openDevTools({
|
||||
mode: 'bottom',
|
||||
}),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
|
|
@ -128,7 +172,7 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
|||
{ type: 'separator' },
|
||||
].concat(isLinux ? [] : [{ role: 'togglefullscreen' }]),
|
||||
},
|
||||
{ role: 'windowMenu' },
|
||||
{ role: 'windowMenu' }
|
||||
);
|
||||
|
||||
template.push(pgadminMenus[pgadminMenus.length - 1]);
|
||||
|
|
@ -136,17 +180,26 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
|||
return Menu.buildFromTemplate(template);
|
||||
}
|
||||
|
||||
export function setupMenu(pgAdminMainScreen, callbacks={}) {
|
||||
ipcMain.on('setMenus', (event, menus)=>{
|
||||
mainMenu = buildMenu(menus, pgAdminMainScreen, callbacks);
|
||||
// this is important because the shortcuts are registered multiple times
|
||||
// when the menu is set multiple times using accelerators.
|
||||
globalShortcut.unregisterAll();
|
||||
function buildAndSetMenus(menus, pgAdminMainScreen, configStore, callbacks={}) {
|
||||
mainMenu = buildMenu(menus, pgAdminMainScreen, configStore, callbacks);
|
||||
if(isMac) {
|
||||
Menu.setApplicationMenu(mainMenu);
|
||||
} else {
|
||||
pgAdminMainScreen.setMenu(mainMenu);
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshMenus(pgAdminMainScreen, configStore, callbacks={}) {
|
||||
buildAndSetMenus(cachedMenus, pgAdminMainScreen, configStore, callbacks);
|
||||
}
|
||||
|
||||
export function setupMenu(pgAdminMainScreen, configStore, callbacks={}) {
|
||||
ipcMain.on('setMenus', (event, menus)=>{
|
||||
// this is important because the shortcuts are registered multiple times
|
||||
// when the menu is set multiple times using accelerators.
|
||||
globalShortcut.unregisterAll();
|
||||
cachedMenus = menus; //It will be used later for refreshing the menus
|
||||
buildAndSetMenus(menus, pgAdminMainScreen, configStore, callbacks);
|
||||
|
||||
ipcMain.on('enable-disable-menu-items', (event, menu, menuItem)=>{
|
||||
const menuItemObj = mainMenu.getMenuItemById(menuItem?.id);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import { app, BrowserWindow, dialog, ipcMain, Menu, shell, screen } from 'electron';
|
||||
import { app, BrowserWindow, dialog, ipcMain, Menu, shell, screen, autoUpdater } from 'electron';
|
||||
import axios from 'axios';
|
||||
import Store from 'electron-store';
|
||||
import fs from 'fs';
|
||||
|
|
@ -17,6 +17,7 @@ import { fileURLToPath } from 'url';
|
|||
import { setupMenu } from './menu.js';
|
||||
import contextMenu from 'electron-context-menu';
|
||||
import { setupDownloader } from './downloader.js';
|
||||
import { setupAutoUpdater, updateConfigAndMenus } from './autoUpdaterHandler.js';
|
||||
|
||||
const configStore = new Store({
|
||||
defaults: {
|
||||
|
|
@ -35,9 +36,13 @@ let configureWindow = null,
|
|||
viewLogWindow = null;
|
||||
|
||||
let serverPort = 5050;
|
||||
let UUID = crypto.randomUUID();
|
||||
|
||||
let appStartTime = (new Date()).getTime();
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
let baseUrl = `http://127.0.0.1:${serverPort}`;
|
||||
|
||||
let docsURLSubStrings = ['www.enterprisedb.com', 'www.postgresql.org', 'www.pgadmin.org', 'help/help'];
|
||||
|
||||
process.env['ELECTRON_ENABLE_SECURITY_WARNINGS'] = false;
|
||||
|
|
@ -45,6 +50,40 @@ process.env['ELECTRON_ENABLE_SECURITY_WARNINGS'] = false;
|
|||
// Paths to the rest of the app
|
||||
let [pythonPath, pgadminFile] = misc.getAppPaths(__dirname);
|
||||
|
||||
const menuCallbacks = {
|
||||
'check_for_updates': ()=>{
|
||||
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', {check_version_update: true});
|
||||
},
|
||||
'restart_to_update': ()=>{
|
||||
forceQuitAndInstallUpdate();
|
||||
},
|
||||
'view_logs': ()=>{
|
||||
if(viewLogWindow === null || viewLogWindow?.isDestroyed()) {
|
||||
viewLogWindow = new BrowserWindow({
|
||||
show: false,
|
||||
width: 800,
|
||||
height: 460,
|
||||
position: 'center',
|
||||
resizable: false,
|
||||
parent: pgAdminMainScreen,
|
||||
icon: '../../assets/pgAdmin4.png',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'other_preload.js'),
|
||||
},
|
||||
});
|
||||
viewLogWindow.loadFile('./src/html/view_log.html');
|
||||
viewLogWindow.once('ready-to-show', ()=>{
|
||||
viewLogWindow.show();
|
||||
});
|
||||
} else {
|
||||
viewLogWindow.hide();
|
||||
viewLogWindow.show();
|
||||
}
|
||||
},
|
||||
'configure': openConfigure,
|
||||
'reloadApp': reloadApp,
|
||||
};
|
||||
|
||||
// Do not allow a second instance of pgAdmin to run.
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
|
|
@ -153,6 +192,28 @@ function reloadApp() {
|
|||
currWin.webContents.reload();
|
||||
}
|
||||
|
||||
|
||||
// Remove auto_update_enabled from configStore on app close or quit
|
||||
function cleanupAutoUpdateFlag() {
|
||||
if (configStore.has('auto_update_enabled')) {
|
||||
configStore.delete('auto_update_enabled');
|
||||
}
|
||||
}
|
||||
|
||||
// This function will force quit and install update and restart the app
|
||||
function forceQuitAndInstallUpdate() {
|
||||
// Disable beforeunload handlers
|
||||
const preventUnload = (event) => {
|
||||
event.preventDefault();
|
||||
pgAdminMainScreen.webContents.off('will-prevent-unload', preventUnload);
|
||||
};
|
||||
pgAdminMainScreen.webContents.on('will-prevent-unload', preventUnload);
|
||||
// Set flag to show notification after restart
|
||||
configStore.set('update_installed', true);
|
||||
cleanupAutoUpdateFlag();
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
|
||||
// This functions is used to start the pgAdmin4 server by spawning a
|
||||
// separate process.
|
||||
function startDesktopMode() {
|
||||
|
|
@ -162,7 +223,6 @@ function startDesktopMode() {
|
|||
return;
|
||||
|
||||
let pingIntervalID;
|
||||
let UUID = crypto.randomUUID();
|
||||
// Set the environment variables so that pgAdmin 4 server
|
||||
// starts listening on the appropriate port.
|
||||
process.env.PGADMIN_INT_PORT = serverPort;
|
||||
|
|
@ -170,7 +230,7 @@ function startDesktopMode() {
|
|||
process.env.PGADMIN_SERVER_MODE = 'OFF';
|
||||
|
||||
// Start Page URL
|
||||
const baseUrl = `http://127.0.0.1:${serverPort}`;
|
||||
baseUrl = `http://127.0.0.1:${serverPort}`;
|
||||
startPageUrl = `${baseUrl}/?key=${UUID}`;
|
||||
serverCheckUrl = `${baseUrl}/misc/ping?key=${UUID}`;
|
||||
|
||||
|
|
@ -307,35 +367,9 @@ function launchPgAdminWindow() {
|
|||
splashWindow.close();
|
||||
pgAdminMainScreen.webContents.session.clearCache();
|
||||
|
||||
setupMenu(pgAdminMainScreen, {
|
||||
'view_logs': ()=>{
|
||||
if(viewLogWindow === null || viewLogWindow?.isDestroyed()) {
|
||||
viewLogWindow = new BrowserWindow({
|
||||
show: false,
|
||||
width: 800,
|
||||
height: 460,
|
||||
position: 'center',
|
||||
resizable: false,
|
||||
parent: pgAdminMainScreen,
|
||||
icon: '../../assets/pgAdmin4.png',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'other_preload.js'),
|
||||
},
|
||||
});
|
||||
viewLogWindow.loadFile('./src/html/view_log.html');
|
||||
viewLogWindow.once('ready-to-show', ()=>{
|
||||
viewLogWindow.show();
|
||||
});
|
||||
} else {
|
||||
viewLogWindow.hide();
|
||||
viewLogWindow.show();
|
||||
}
|
||||
},
|
||||
'configure': openConfigure,
|
||||
'reloadApp': reloadApp,
|
||||
});
|
||||
setupMenu(pgAdminMainScreen, configStore, menuCallbacks);
|
||||
|
||||
setupDownloader();
|
||||
setupDownloader()
|
||||
|
||||
pgAdminMainScreen.loadURL(startPageUrl);
|
||||
|
||||
|
|
@ -346,6 +380,15 @@ function launchPgAdminWindow() {
|
|||
|
||||
pgAdminMainScreen.show();
|
||||
|
||||
setupAutoUpdater({
|
||||
pgAdminMainScreen,
|
||||
configStore,
|
||||
menuCallbacks,
|
||||
baseUrl,
|
||||
UUID,
|
||||
forceQuitAndInstallUpdate,
|
||||
});
|
||||
|
||||
pgAdminMainScreen.webContents.setWindowOpenHandler(({url})=>{
|
||||
let openDocsInBrowser = configStore.get('openDocsInBrowser', true);
|
||||
let isDocURL = false;
|
||||
|
|
@ -377,18 +420,50 @@ function launchPgAdminWindow() {
|
|||
});
|
||||
|
||||
pgAdminMainScreen.on('closed', ()=>{
|
||||
cleanupAutoUpdateFlag();
|
||||
misc.cleanupAndQuitApp();
|
||||
});
|
||||
|
||||
pgAdminMainScreen.on('close', () => {
|
||||
configStore.set('bounds', pgAdminMainScreen.getBounds());
|
||||
updateConfigAndMenus('error-close', configStore, pgAdminMainScreen, menuCallbacks);
|
||||
pgAdminMainScreen.removeAllListeners('close');
|
||||
pgAdminMainScreen.close();
|
||||
});
|
||||
|
||||
// Notify if update was installed (fix: always check after main window is ready)
|
||||
notifyUpdateInstalled();
|
||||
}
|
||||
|
||||
let splashWindow;
|
||||
|
||||
// Helper to notify update installed after restart
|
||||
function notifyUpdateInstalled() {
|
||||
if (configStore.get('update_installed')) {
|
||||
try {
|
||||
// Notify renderer
|
||||
if (pgAdminMainScreen) {
|
||||
misc.writeServerLog('[Auto-Updater]: Update installed successfully...');
|
||||
setTimeout(() => {
|
||||
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', {update_installed: true});
|
||||
}, 10000);
|
||||
} else {
|
||||
// If main screen not ready, wait and send after it's created
|
||||
app.once('browser-window-created', (event, window) => {
|
||||
misc.writeServerLog('[Auto-Updater]: Update installed successfully...');
|
||||
setTimeout(() => {
|
||||
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', {update_installed: true});
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
// Reset the flag
|
||||
configStore.set('update_installed', false);
|
||||
} catch (err) {
|
||||
misc.writeServerLog(`[Auto-Updater]: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setup preload events.
|
||||
ipcMain.handle('showOpenDialog', (e, options) => dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender), options));
|
||||
ipcMain.handle('showSaveDialog', (e, options) => dialog.showSaveDialog(BrowserWindow.fromWebContents(e.sender), options));
|
||||
|
|
@ -406,7 +481,7 @@ ipcMain.on('restartApp', ()=>{
|
|||
app.relaunch();
|
||||
app.exit(0);
|
||||
});
|
||||
ipcMain.on('log', (_e, text) => ()=>{
|
||||
ipcMain.on('log', (_e, text) => {
|
||||
misc.writeServerLog(text);
|
||||
});
|
||||
ipcMain.on('focus', (e) => {
|
||||
|
|
@ -428,6 +503,7 @@ ipcMain.handle('checkPortAvailable', async (_e, fixedPort)=>{
|
|||
});
|
||||
ipcMain.handle('openConfigure', openConfigure);
|
||||
|
||||
|
||||
app.whenReady().then(() => {
|
||||
splashWindow = new BrowserWindow({
|
||||
transparent: true,
|
||||
|
|
|
|||
|
|
@ -32,4 +32,10 @@ contextBridge.exposeInMainWorld('electronUI', {
|
|||
downloadStreamSaveEnd: (...args) => ipcRenderer.send('download-stream-save-end', ...args),
|
||||
downloadBase64UrlData: (...args) => ipcRenderer.invoke('download-base64-url-data', ...args),
|
||||
downloadTextData: (...args) => ipcRenderer.invoke('download-text-data', ...args),
|
||||
//Auto-updater related functions
|
||||
sendDataForAppUpdate: (data) => ipcRenderer.send('sendDataForAppUpdate', data),
|
||||
notifyAppAutoUpdate: (callback) => {
|
||||
ipcRenderer.removeAllListeners('notifyAppAutoUpdate'); // Clean up previous listeners
|
||||
ipcRenderer.on('notifyAppAutoUpdate', (_, data) => callback(data));
|
||||
},
|
||||
});
|
||||
|
|
@ -15,6 +15,7 @@ import { send_heartbeat, stop_heartbeat } from './heartbeat';
|
|||
import getApiInstance from '../../../static/js/api_instance';
|
||||
import usePreferences, { setupPreferenceBroadcast } from '../../../preferences/static/js/store';
|
||||
import checkNodeVisibility from '../../../static/js/check_node_visibility';
|
||||
import {appAutoUpdateNotifier} from '../../../static/js/helpers/appAutoUpdateNotifier';
|
||||
|
||||
define('pgadmin.browser', [
|
||||
'sources/gettext', 'sources/url_for', 'sources/pgadmin',
|
||||
|
|
@ -272,12 +273,34 @@ define('pgadmin.browser', [
|
|||
checkMasterPassword(data, self.masterpass_callback_queue, cancel_callback);
|
||||
},
|
||||
|
||||
check_version_update: function() {
|
||||
check_version_update: async function(trigger_update_check=false) {
|
||||
getApiInstance().get(
|
||||
url_for('misc.upgrade_check')
|
||||
url_for('misc.upgrade_check') + '?trigger_update_check=' + trigger_update_check
|
||||
).then((res)=> {
|
||||
const data = res.data.data;
|
||||
if(data.outdated) {
|
||||
window.electronUI?.sendDataForAppUpdate({
|
||||
'check_for_updates': data.check_for_auto_updates,
|
||||
});
|
||||
const isDesktopWithAutoUpdate = pgAdmin.server_mode == 'False' && data.check_for_auto_updates && data.auto_update_url !== '';
|
||||
const isUpdateAvailable = data.outdated && data.upgrade_version_int > data.current_version_int;
|
||||
const noUpdateMessage = 'No update available...';
|
||||
// This is for desktop installers whose auto_update_url is mentioned in https://www.pgadmin.org/versions.json
|
||||
if (isDesktopWithAutoUpdate) {
|
||||
if (isUpdateAvailable) {
|
||||
const message = `${gettext('You are currently running version %s of %s, however the current version is %s.', data.current_version, data.product_name, data.upgrade_version)}`;
|
||||
appAutoUpdateNotifier(
|
||||
message,
|
||||
'warning',
|
||||
() => {
|
||||
window.electronUI?.sendDataForAppUpdate(data);
|
||||
},
|
||||
null,
|
||||
'Update available',
|
||||
'download_update'
|
||||
);
|
||||
}
|
||||
} else if(data.outdated) {
|
||||
//This is for server mode or auto-update not supported desktop installer or not mentioned auto_update_url
|
||||
pgAdmin.Browser.notifier.warning(
|
||||
`
|
||||
${gettext('You are currently running version %s of %s, <br/>however the current version is %s.', data.current_version, data.product_name, data.upgrade_version)}
|
||||
|
|
@ -287,9 +310,14 @@ define('pgadmin.browser', [
|
|||
null
|
||||
);
|
||||
}
|
||||
|
||||
}).catch(function() {
|
||||
// Suppress any errors
|
||||
// If the user manually triggered a check for updates (trigger_update_check is true)
|
||||
// and no update is available (data.outdated is false), show an info notification.
|
||||
if (!data.outdated && trigger_update_check){
|
||||
appAutoUpdateNotifier(noUpdateMessage, 'info', null, 10000);
|
||||
}
|
||||
}).catch((error)=>{
|
||||
console.error('Error during version check', error);
|
||||
pgAdmin.Browser.notifier.error(gettext(`${error.response?.data?.errormsg || error?.message}`));
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from pgadmin.utils.csrf import pgCSRFProtect
|
|||
from pgadmin.utils.session import cleanup_session_files
|
||||
from pgadmin.misc.themes import get_all_themes
|
||||
from pgadmin.utils.ajax import precondition_required, make_json_response, \
|
||||
internal_server_error
|
||||
internal_server_error, make_response
|
||||
from pgadmin.utils.heartbeat import log_server_heartbeat, \
|
||||
get_server_heartbeat, stop_server_heartbeat
|
||||
import config
|
||||
|
|
@ -32,6 +32,7 @@ import os
|
|||
import sys
|
||||
import ssl
|
||||
from urllib.request import urlopen
|
||||
from urllib.parse import unquote
|
||||
from pgadmin.settings import get_setting, store_setting
|
||||
|
||||
MODULE_NAME = 'misc'
|
||||
|
|
@ -171,7 +172,7 @@ class MiscModule(PgAdminModule):
|
|||
return ['misc.ping', 'misc.index', 'misc.cleanup',
|
||||
'misc.validate_binary_path', 'misc.log_heartbeat',
|
||||
'misc.stop_heartbeat', 'misc.get_heartbeat',
|
||||
'misc.upgrade_check']
|
||||
'misc.upgrade_check', 'misc.auto_update']
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
|
|
@ -343,19 +344,28 @@ def validate_binary_path():
|
|||
methods=['GET'])
|
||||
@pga_login_required
|
||||
def upgrade_check():
|
||||
# Get the current version info from the website, and flash a message if
|
||||
# the user is out of date, and the check is enabled.
|
||||
ret = {
|
||||
"outdated": False,
|
||||
}
|
||||
"""
|
||||
Check for application updates and return update metadata to the client.
|
||||
- Compares current version with remote version data.
|
||||
- Supports auto-update in desktop mode.
|
||||
"""
|
||||
# Determine if this check was manually triggered by the user
|
||||
trigger_update_check = (request.args.get('trigger_update_check', 'false')
|
||||
.lower() == 'true')
|
||||
|
||||
platform = None
|
||||
ret = {"outdated": False}
|
||||
|
||||
if config.UPGRADE_CHECK_ENABLED:
|
||||
last_check = get_setting('LastUpdateCheck', default='0')
|
||||
today = time.strftime('%Y%m%d')
|
||||
if int(last_check) < int(today):
|
||||
|
||||
data = None
|
||||
url = '%s?version=%s' % (
|
||||
config.UPGRADE_CHECK_URL, config.APP_VERSION)
|
||||
current_app.logger.debug('Checking version data at: %s' % url)
|
||||
|
||||
# Attempt to fetch upgrade data from remote URL
|
||||
try:
|
||||
# Do not wait for more than 5 seconds.
|
||||
# It stuck on rendering the browser.html, while working in the
|
||||
|
|
@ -384,11 +394,42 @@ def upgrade_check():
|
|||
'Exception when checking for update')
|
||||
return internal_server_error('Failed to check for update')
|
||||
|
||||
if data is not None and \
|
||||
data[config.UPGRADE_CHECK_KEY]['version_int'] > \
|
||||
config.APP_VERSION_INT:
|
||||
if data:
|
||||
# Determine platform
|
||||
if sys.platform == 'darwin':
|
||||
platform = 'macos'
|
||||
elif sys.platform == 'win32':
|
||||
platform = 'windows'
|
||||
|
||||
upgrade_version_int = data[config.UPGRADE_CHECK_KEY]['version_int']
|
||||
auto_update_url_exists = data[config.UPGRADE_CHECK_KEY][
|
||||
'auto_update_url'][platform] != ''
|
||||
|
||||
# Construct common response dicts for auto-update support
|
||||
auto_update_common_res = {
|
||||
"check_for_auto_updates": True,
|
||||
"auto_update_url": data[config.UPGRADE_CHECK_KEY][
|
||||
'auto_update_url'][platform],
|
||||
"platform": platform,
|
||||
"installer_type": config.UPGRADE_CHECK_KEY,
|
||||
"current_version": config.APP_VERSION,
|
||||
"upgrade_version": data[config.UPGRADE_CHECK_KEY]['version'],
|
||||
"current_version_int": config.APP_VERSION_INT,
|
||||
"upgrade_version_int": upgrade_version_int,
|
||||
"product_name": config.APP_NAME,
|
||||
}
|
||||
|
||||
# Check for updates if the last check was before today(daily check)
|
||||
if int(last_check) < int(today):
|
||||
# App is outdated
|
||||
if upgrade_version_int > config.APP_VERSION_INT:
|
||||
if not config.SERVER_MODE and auto_update_url_exists:
|
||||
ret = {**auto_update_common_res, "outdated": True}
|
||||
else:
|
||||
# Auto-update unsupported
|
||||
ret = {
|
||||
"outdated": True,
|
||||
"check_for_auto_updates": False,
|
||||
"current_version": config.APP_VERSION,
|
||||
"upgrade_version": data[config.UPGRADE_CHECK_KEY][
|
||||
'version'],
|
||||
|
|
@ -396,6 +437,45 @@ def upgrade_check():
|
|||
"download_url": data[config.UPGRADE_CHECK_KEY][
|
||||
'download_url']
|
||||
}
|
||||
# App is up-to-date, but auto-update should be enabled
|
||||
elif (upgrade_version_int == config.APP_VERSION_INT and
|
||||
not config.SERVER_MODE and auto_update_url_exists):
|
||||
ret = {**auto_update_common_res, "outdated": False}
|
||||
# If already checked today,
|
||||
# return auto-update info only if supported
|
||||
elif (int(last_check) == int(today) and
|
||||
not config.SERVER_MODE and auto_update_url_exists):
|
||||
# Check for updates when triggered by user
|
||||
# and new version is available
|
||||
if (upgrade_version_int > config.APP_VERSION_INT and
|
||||
trigger_update_check):
|
||||
ret = {**auto_update_common_res, "outdated": True}
|
||||
else:
|
||||
ret = {**auto_update_common_res, "outdated": False}
|
||||
|
||||
store_setting('LastUpdateCheck', today)
|
||||
return make_json_response(data=ret)
|
||||
|
||||
|
||||
@blueprint.route("/auto_update/<current_version_int>/<latest_version>"
|
||||
"/<latest_version_int>/<product_name>/<path:ftp_url>/",
|
||||
methods=['GET'])
|
||||
@pgCSRFProtect.exempt
|
||||
def auto_update(current_version_int, latest_version, latest_version_int,
|
||||
product_name, ftp_url):
|
||||
"""
|
||||
Get auto-update information for the desktop app.
|
||||
|
||||
Returns update metadata (download URL and version name)
|
||||
if a newer version is available. Responds with HTTP 204
|
||||
if the current version is up to date.
|
||||
"""
|
||||
if latest_version_int > current_version_int:
|
||||
update_info = {
|
||||
'url': unquote(ftp_url),
|
||||
'name': f'{product_name} v{latest_version}',
|
||||
}
|
||||
current_app.logger.debug(update_info)
|
||||
return make_response(response=update_info, status=200)
|
||||
else:
|
||||
return make_response(status=204)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class SettingsModule(PgAdminModule):
|
|||
'file_items': [
|
||||
MenuItem(
|
||||
name='mnu_resetlayout',
|
||||
priority=998,
|
||||
priority=997,
|
||||
module="pgAdmin.Settings",
|
||||
callback='show',
|
||||
label=gettext('Reset Layout')
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { useWorkspace, WorkspaceProvider } from '../../misc/workspaces/static/js
|
|||
import { PgAdminProvider, usePgAdmin } from './PgAdminProvider';
|
||||
import PreferencesComponent from '../../preferences/static/js/components/PreferencesComponent';
|
||||
import { ApplicationStateProvider } from '../../settings/static/ApplicationStateProvider';
|
||||
|
||||
import { appAutoUpdateNotifier } from './helpers/appAutoUpdateNotifier';
|
||||
|
||||
const objectExplorerGroup = {
|
||||
tabLocked: true,
|
||||
|
|
@ -181,6 +181,36 @@ export default function BrowserComponent({pgAdmin}) {
|
|||
isNewTab: true,
|
||||
});
|
||||
|
||||
// Called when Install and Restart btn called for auto-update install
|
||||
function installUpdate() {
|
||||
if (window.electronUI) {
|
||||
window.electronUI.sendDataForAppUpdate({
|
||||
'install_update_now': true
|
||||
});
|
||||
}}
|
||||
|
||||
// Listen for auto-update events from the Electron main process and display notifications
|
||||
// to the user based on the update status (e.g., update available, downloading, downloaded, installed, or error).
|
||||
if (window.electronUI && typeof window.electronUI.notifyAppAutoUpdate === 'function') {
|
||||
window.electronUI.notifyAppAutoUpdate((data)=>{
|
||||
if (data?.check_version_update) {
|
||||
pgAdmin.Browser.check_version_update(true);
|
||||
} else if (data.update_downloading) {
|
||||
appAutoUpdateNotifier('Update downloading...', 'info', null, 10000);
|
||||
} else if (data.no_update_available) {
|
||||
appAutoUpdateNotifier('No update available...', 'info', null, 10000);
|
||||
} else if (data.update_downloaded) {
|
||||
const UPDATE_DOWNLOADED_MESSAGE = gettext('An update is ready. Restart the app now to install it, or later to keep using the current version.');
|
||||
appAutoUpdateNotifier(UPDATE_DOWNLOADED_MESSAGE, 'warning', installUpdate, null, 'Update downloaded', 'update_downloaded');
|
||||
} else if (data.error) {
|
||||
appAutoUpdateNotifier(`${data.errMsg}`, 'error');
|
||||
} else if (data.update_installed) {
|
||||
const UPDATE_INSTALLED_MESSAGE = gettext('Update installed successfully!');
|
||||
appAutoUpdateNotifier(UPDATE_INSTALLED_MESSAGE, 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
if(uiReady) {
|
||||
pgAdmin?.Browser?.uiloaded?.();
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ export default function(basicSettings) {
|
|||
main: '#eea236',
|
||||
light: '#fce5c5',
|
||||
contrastText: '#000',
|
||||
hoverMain: darken('#eea236', 0.1),
|
||||
hoverBorderColor: darken('#eea236', 0.1),
|
||||
},
|
||||
info: {
|
||||
main: '#fde74c',
|
||||
|
|
|
|||
|
|
@ -1289,6 +1289,7 @@ const StyledNotifierMessageBox = styled(Box)(({theme}) => ({
|
|||
backgroundColor: theme.palette.warning.light,
|
||||
'& .FormFooter-iconWarning': {
|
||||
color: theme.palette.warning.main,
|
||||
marginBottom: theme.spacing(8),
|
||||
},
|
||||
},
|
||||
'& .FormFooter-message': {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import CloseIcon from '@mui/icons-material/CloseRounded';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DefaultButton, PgIconButton } from '../components/Buttons';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
|
||||
const StyledBox = styled(Box)(({theme}) => ({
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: '0.25rem 1rem 1rem',
|
||||
minWidth: '325px',
|
||||
maxWidth: '400px',
|
||||
...theme.mixins.panelBorder.all,
|
||||
'&.UpdateWarningNotifier-containerWarning': {
|
||||
borderColor: theme.palette.warning.main,
|
||||
backgroundColor: theme.palette.warning.light,
|
||||
},
|
||||
'& .UpdateWarningNotifier-containerHeader': {
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontWeight: 'bold',
|
||||
alignItems: 'center',
|
||||
borderTopLeftRadius: 'inherit',
|
||||
borderTopRightRadius: 'inherit',
|
||||
'& .UpdateWarningNotifier-iconWarning': {
|
||||
color: theme.palette.warning.main,
|
||||
},
|
||||
},
|
||||
'&.UpdateWarningNotifier-containerBody': {
|
||||
marginTop: '1rem',
|
||||
overflowWrap: 'break-word',
|
||||
},
|
||||
}));
|
||||
|
||||
const activeWarningKeys = new Set();
|
||||
|
||||
function UpdateWarningNotifier({desc, title, onClose, onClick, status, uniqueKey}) {
|
||||
const handleClose = () => {
|
||||
if (onClose) onClose();
|
||||
if (uniqueKey) {
|
||||
activeWarningKeys.delete(uniqueKey);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<StyledBox className={'UpdateWarningNotifier-containerWarning'} data-test={'Update-popup-warning'}>
|
||||
<Box display="flex" justifyContent="space-between" className='UpdateWarningNotifier-containerHeader'>
|
||||
<Box marginRight={'1rem'}>{title}</Box>
|
||||
<PgIconButton size="xs" noBorder icon={<CloseIcon />} onClick={handleClose} title={'Close'} className={'UpdateWarningNotifier-iconWarning'} />
|
||||
</Box>
|
||||
<Box className='UpdateWarningNotifier-containerBody'>
|
||||
{desc && <Box>{desc}</Box>}
|
||||
<Box display="flex">
|
||||
{onClick && <Box marginTop={'1rem'} display="flex">
|
||||
<DefaultButton color={'warning'} onClick={()=>{
|
||||
onClick();
|
||||
handleClose();
|
||||
}}>{status == 'download_update' ? 'Download Update' : 'Install and Restart'}</DefaultButton>
|
||||
</Box>}
|
||||
{status == 'update_downloaded' && <Box marginTop={'1rem'} display="flex" marginLeft={'1rem'}>
|
||||
<DefaultButton color={'default'} onClick={()=>{
|
||||
handleClose();
|
||||
}}>Install Later</DefaultButton>
|
||||
</Box>}
|
||||
</Box>
|
||||
</Box>
|
||||
</StyledBox>
|
||||
);
|
||||
}
|
||||
UpdateWarningNotifier.propTypes = {
|
||||
desc: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
status: PropTypes.string,
|
||||
uniqueKey: PropTypes.string,
|
||||
};
|
||||
|
||||
export function appAutoUpdateNotifier(desc, type, onClick, hideDuration=null, title='', status='download_update') {
|
||||
const uniqueKey = `${title}::${desc}`;
|
||||
|
||||
// Check if this warning is already active except error type
|
||||
if (activeWarningKeys.has(uniqueKey) && type !== 'error') {
|
||||
// Already showing, do not show again
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark this warning as active
|
||||
activeWarningKeys.add(uniqueKey);
|
||||
if (type == 'warning') {
|
||||
pgAdmin.Browser.notifier.notify(
|
||||
<UpdateWarningNotifier
|
||||
title={title}
|
||||
desc={desc}
|
||||
onClick={onClick}
|
||||
status={status}
|
||||
uniqueKey={uniqueKey}
|
||||
onClose={() => {
|
||||
// Remove from active keys when closed
|
||||
activeWarningKeys.delete(uniqueKey);
|
||||
}}
|
||||
/>, null
|
||||
);
|
||||
} else if(type == 'success') {
|
||||
pgAdmin.Browser.notifier.success(desc, hideDuration);
|
||||
} else if(type == 'info') {
|
||||
pgAdmin.Browser.notifier.info(desc, hideDuration);
|
||||
} else if(type == 'error') {
|
||||
pgAdmin.Browser.notifier.error(desc, hideDuration);
|
||||
}
|
||||
|
||||
// Remove from active keys for valid hideDuration passed in args
|
||||
setTimeout(()=>{
|
||||
hideDuration && activeWarningKeys.delete(uniqueKey);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue