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
|
# Include only platform-independent builds in all
|
||||||
all: docs pip src
|
all: docs pip src
|
||||||
|
|
||||||
|
# Add BUILD_OPTS variable to pass arguments
|
||||||
appbundle:
|
appbundle:
|
||||||
./pkg/mac/build.sh
|
./pkg/mac/build.sh $(BUILD_OPTS)
|
||||||
|
|
||||||
install-node:
|
install-node:
|
||||||
cd web && yarn install
|
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/),
|
will be available on Unix systems (~/.local/share/pgadmin/),
|
||||||
on Mac OS X (~/Library/Preferences/pgadmin),
|
on Mac OS X (~/Library/Preferences/pgadmin),
|
||||||
and on Windows (%APPDATA%/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
|
*notarization.conf* and set the values accordingly. Note that notarization
|
||||||
will fail if the code isn't signed.
|
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
|
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
|
This will create the python virtual environment and install all the required
|
||||||
python modules mentioned in the requirements file using pip, build the
|
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.
|
directory.
|
||||||
|
|
|
||||||
|
|
@ -385,6 +385,24 @@ _codesign_bundle() {
|
||||||
-i org.pgadmin.pgadmin4 \
|
-i org.pgadmin.pgadmin4 \
|
||||||
--sign "${DEVELOPER_ID}" \
|
--sign "${DEVELOPER_ID}" \
|
||||||
"${BUNDLE_DIR}"
|
"${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() {
|
_create_dmg() {
|
||||||
|
|
@ -426,18 +444,23 @@ _codesign_dmg() {
|
||||||
"${DMG_NAME}"
|
"${DMG_NAME}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_notarize_pkg() {
|
_notarize_pkg() {
|
||||||
|
local FILE_NAME="$1"
|
||||||
|
local STAPLE_TARGET="$2"
|
||||||
|
local FILE_LABEL="$3"
|
||||||
|
|
||||||
if [ "${CODESIGN}" -eq 0 ]; then
|
if [ "${CODESIGN}" -eq 0 ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Uploading DMG for Notarization ..."
|
echo "Uploading ${FILE_LABEL} for Notarization ..."
|
||||||
STATUS=$(xcrun notarytool submit "${DMG_NAME}" \
|
STATUS=$(xcrun notarytool submit "${FILE_NAME}" \
|
||||||
--team-id "${DEVELOPER_TEAM_ID}" \
|
--team-id "${DEVELOPER_TEAM_ID}" \
|
||||||
--apple-id "${DEVELOPER_USER}" \
|
--apple-id "${DEVELOPER_USER}" \
|
||||||
--password "${DEVELOPER_ASP}" 2>&1)
|
--password "${DEVELOPER_ASP}" 2>&1)
|
||||||
|
|
||||||
|
echo "${STATUS}"
|
||||||
|
|
||||||
# Get the submission ID
|
# Get the submission ID
|
||||||
SUBMISSION_ID=$(echo "${STATUS}" | awk -F ': ' '/id:/ { print $2; exit; }')
|
SUBMISSION_ID=$(echo "${STATUS}" | awk -F ': ' '/id:/ { print $2; exit; }')
|
||||||
echo "Notarization submission ID: ${SUBMISSION_ID}"
|
echo "Notarization submission ID: ${SUBMISSION_ID}"
|
||||||
|
|
@ -461,11 +484,28 @@ _notarize_pkg() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Staple the notarization
|
# Staple the notarization
|
||||||
echo "Stapling the notarization to the pgAdmin DMG..."
|
echo "Stapling the notarization to the ${FILE_LABEL}..."
|
||||||
if ! xcrun stapler staple "${DMG_NAME}"; then
|
if ! xcrun stapler staple "${STAPLE_TARGET}"; then
|
||||||
echo "Stapling failed."
|
echo "Stapling failed."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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."
|
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
|
export PGADMIN_PYTHON_VERSION=3.13.1
|
||||||
fi
|
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
|
# shellcheck disable=SC1091
|
||||||
source "${SCRIPT_DIR}/build-functions.sh"
|
source "${SCRIPT_DIR}/build-functions.sh"
|
||||||
|
|
||||||
|
|
@ -69,6 +95,16 @@ _complete_bundle
|
||||||
_generate_sbom
|
_generate_sbom
|
||||||
_codesign_binaries
|
_codesign_binaries
|
||||||
_codesign_bundle
|
_codesign_bundle
|
||||||
_create_dmg
|
|
||||||
_codesign_dmg
|
# Handle ZIP creation if requested
|
||||||
_notarize_pkg
|
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_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 isMac = process.platform == 'darwin';
|
||||||
const isLinux = process.platform == 'linux';
|
const isLinux = process.platform == 'linux';
|
||||||
let mainMenu;
|
let mainMenu;
|
||||||
|
let cachedMenus;
|
||||||
|
|
||||||
// Use to convert shortcut to accelerator for electron.
|
// Use to convert shortcut to accelerator for electron.
|
||||||
function convertShortcutToAccelerator({ control, meta, shift, alt, key } = {}) {
|
function convertShortcutToAccelerator({ control, meta, shift, alt, key } = {}) {
|
||||||
|
|
@ -29,14 +30,11 @@ function convertShortcutToAccelerator({ control, meta, shift, alt, key } = {}) {
|
||||||
return [...mods, k].join('+');
|
return [...mods, k].join('+');
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
// Binds click events to all menu and submenu items recursively.
|
||||||
const template = [];
|
function bindMenuClicks(pgadminMenus, pgAdminMainScreen) {
|
||||||
|
return pgadminMenus.map((menuItem) => ({
|
||||||
// bind all menus click event.
|
|
||||||
pgadminMenus = pgadminMenus.map((menuItem)=>{
|
|
||||||
return {
|
|
||||||
...menuItem,
|
...menuItem,
|
||||||
submenu: menuItem.submenu?.map((subMenuItem)=>{
|
submenu: menuItem.submenu?.map((subMenuItem) => {
|
||||||
const smName = `${menuItem.name}_${subMenuItem.name}`;
|
const smName = `${menuItem.name}_${subMenuItem.name}`;
|
||||||
return {
|
return {
|
||||||
...subMenuItem,
|
...subMenuItem,
|
||||||
|
|
@ -50,8 +48,7 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
||||||
}
|
}
|
||||||
pgAdminMainScreen.webContents.send('menu-click', smName);
|
pgAdminMainScreen.webContents.send('menu-click', smName);
|
||||||
},
|
},
|
||||||
submenu: subMenuItem.submenu?.map((deeperSubMenuItem)=>{
|
submenu: subMenuItem.submenu?.map((deeperSubMenuItem) => ({
|
||||||
return {
|
|
||||||
...deeperSubMenuItem,
|
...deeperSubMenuItem,
|
||||||
accelerator: convertShortcutToAccelerator(deeperSubMenuItem.shortcut),
|
accelerator: convertShortcutToAccelerator(deeperSubMenuItem.shortcut),
|
||||||
click: (_menuItem, _browserWindow, event)=>{
|
click: (_menuItem, _browserWindow, event)=>{
|
||||||
|
|
@ -63,26 +60,69 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
||||||
}
|
}
|
||||||
pgAdminMainScreen.webContents.send('menu-click', `${smName}_${deeperSubMenuItem.name}`);
|
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();
|
let menuFile = pgadminMenus.shift();
|
||||||
|
|
||||||
|
// macOS-specific menu modifications
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
// Remove About pgAdmin 4 from help menu and add it to the top of menuFile submenu.
|
handleAutoUpdateMenu(menuFile, configStore, callbacks);
|
||||||
const helpMenu = pgadminMenus.find((menu) => menu.name == 'help');
|
moveAboutMenuToTop(pgadminMenus, menuFile);
|
||||||
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' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template.push({
|
template.push({
|
||||||
|
|
@ -90,24 +130,22 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
||||||
submenu: [
|
submenu: [
|
||||||
...menuFile.submenu,
|
...menuFile.submenu,
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{ label: 'View Logs...', click: callbacks['view_logs'] },
|
||||||
label: 'View Logs...', click: callbacks['view_logs'],
|
{ label: 'Configure runtime...', click: callbacks['configure'] },
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Configure runtime...', click: callbacks['configure'],
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
...(isMac ? [
|
...(isMac
|
||||||
|
? [
|
||||||
{ role: 'hide' },
|
{ role: 'hide' },
|
||||||
{ role: 'hideOthers' },
|
{ role: 'hideOthers' },
|
||||||
{ role: 'unhide' },
|
{ role: 'unhide' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
] : []),
|
]
|
||||||
|
: []),
|
||||||
{ role: 'quit' },
|
{ role: 'quit' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if(isMac) {
|
if (isMac) {
|
||||||
template[0].label = app.name;
|
template[0].label = app.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,8 +157,14 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
||||||
{
|
{
|
||||||
label: 'View',
|
label: 'View',
|
||||||
submenu: [
|
submenu: [
|
||||||
{ label: 'Reload', click: callbacks['reloadApp']},
|
{ 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' },
|
{ type: 'separator' },
|
||||||
{ role: 'resetZoom' },
|
{ role: 'resetZoom' },
|
||||||
{ role: 'zoomIn' },
|
{ role: 'zoomIn' },
|
||||||
|
|
@ -128,25 +172,34 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
].concat(isLinux ? [] : [{ role: 'togglefullscreen' }]),
|
].concat(isLinux ? [] : [{ role: 'togglefullscreen' }]),
|
||||||
},
|
},
|
||||||
{ role: 'windowMenu' },
|
{ role: 'windowMenu' }
|
||||||
);
|
);
|
||||||
|
|
||||||
template.push(pgadminMenus[pgadminMenus.length-1]);
|
template.push(pgadminMenus[pgadminMenus.length - 1]);
|
||||||
|
|
||||||
return Menu.buildFromTemplate(template);
|
return Menu.buildFromTemplate(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupMenu(pgAdminMainScreen, callbacks={}) {
|
function buildAndSetMenus(menus, pgAdminMainScreen, configStore, callbacks={}) {
|
||||||
ipcMain.on('setMenus', (event, menus)=>{
|
mainMenu = buildMenu(menus, pgAdminMainScreen, configStore, callbacks);
|
||||||
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();
|
|
||||||
if(isMac) {
|
if(isMac) {
|
||||||
Menu.setApplicationMenu(mainMenu);
|
Menu.setApplicationMenu(mainMenu);
|
||||||
} else {
|
} else {
|
||||||
pgAdminMainScreen.setMenu(mainMenu);
|
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)=>{
|
ipcMain.on('enable-disable-menu-items', (event, menu, menuItem)=>{
|
||||||
const menuItemObj = mainMenu.getMenuItemById(menuItem?.id);
|
const menuItemObj = mainMenu.getMenuItemById(menuItem?.id);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
// This software is released under the PostgreSQL Licence
|
// 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 axios from 'axios';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
@ -17,6 +17,7 @@ import { fileURLToPath } from 'url';
|
||||||
import { setupMenu } from './menu.js';
|
import { setupMenu } from './menu.js';
|
||||||
import contextMenu from 'electron-context-menu';
|
import contextMenu from 'electron-context-menu';
|
||||||
import { setupDownloader } from './downloader.js';
|
import { setupDownloader } from './downloader.js';
|
||||||
|
import { setupAutoUpdater, updateConfigAndMenus } from './autoUpdaterHandler.js';
|
||||||
|
|
||||||
const configStore = new Store({
|
const configStore = new Store({
|
||||||
defaults: {
|
defaults: {
|
||||||
|
|
@ -35,9 +36,13 @@ let configureWindow = null,
|
||||||
viewLogWindow = null;
|
viewLogWindow = null;
|
||||||
|
|
||||||
let serverPort = 5050;
|
let serverPort = 5050;
|
||||||
|
let UUID = crypto.randomUUID();
|
||||||
|
|
||||||
let appStartTime = (new Date()).getTime();
|
let appStartTime = (new Date()).getTime();
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
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'];
|
let docsURLSubStrings = ['www.enterprisedb.com', 'www.postgresql.org', 'www.pgadmin.org', 'help/help'];
|
||||||
|
|
||||||
process.env['ELECTRON_ENABLE_SECURITY_WARNINGS'] = false;
|
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
|
// Paths to the rest of the app
|
||||||
let [pythonPath, pgadminFile] = misc.getAppPaths(__dirname);
|
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.
|
// Do not allow a second instance of pgAdmin to run.
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
|
|
@ -153,6 +192,28 @@ function reloadApp() {
|
||||||
currWin.webContents.reload();
|
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
|
// This functions is used to start the pgAdmin4 server by spawning a
|
||||||
// separate process.
|
// separate process.
|
||||||
function startDesktopMode() {
|
function startDesktopMode() {
|
||||||
|
|
@ -162,7 +223,6 @@ function startDesktopMode() {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let pingIntervalID;
|
let pingIntervalID;
|
||||||
let UUID = crypto.randomUUID();
|
|
||||||
// Set the environment variables so that pgAdmin 4 server
|
// Set the environment variables so that pgAdmin 4 server
|
||||||
// starts listening on the appropriate port.
|
// starts listening on the appropriate port.
|
||||||
process.env.PGADMIN_INT_PORT = serverPort;
|
process.env.PGADMIN_INT_PORT = serverPort;
|
||||||
|
|
@ -170,7 +230,7 @@ function startDesktopMode() {
|
||||||
process.env.PGADMIN_SERVER_MODE = 'OFF';
|
process.env.PGADMIN_SERVER_MODE = 'OFF';
|
||||||
|
|
||||||
// Start Page URL
|
// Start Page URL
|
||||||
const baseUrl = `http://127.0.0.1:${serverPort}`;
|
baseUrl = `http://127.0.0.1:${serverPort}`;
|
||||||
startPageUrl = `${baseUrl}/?key=${UUID}`;
|
startPageUrl = `${baseUrl}/?key=${UUID}`;
|
||||||
serverCheckUrl = `${baseUrl}/misc/ping?key=${UUID}`;
|
serverCheckUrl = `${baseUrl}/misc/ping?key=${UUID}`;
|
||||||
|
|
||||||
|
|
@ -307,35 +367,9 @@ function launchPgAdminWindow() {
|
||||||
splashWindow.close();
|
splashWindow.close();
|
||||||
pgAdminMainScreen.webContents.session.clearCache();
|
pgAdminMainScreen.webContents.session.clearCache();
|
||||||
|
|
||||||
setupMenu(pgAdminMainScreen, {
|
setupMenu(pgAdminMainScreen, configStore, menuCallbacks);
|
||||||
'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,
|
|
||||||
});
|
|
||||||
|
|
||||||
setupDownloader();
|
setupDownloader()
|
||||||
|
|
||||||
pgAdminMainScreen.loadURL(startPageUrl);
|
pgAdminMainScreen.loadURL(startPageUrl);
|
||||||
|
|
||||||
|
|
@ -346,6 +380,15 @@ function launchPgAdminWindow() {
|
||||||
|
|
||||||
pgAdminMainScreen.show();
|
pgAdminMainScreen.show();
|
||||||
|
|
||||||
|
setupAutoUpdater({
|
||||||
|
pgAdminMainScreen,
|
||||||
|
configStore,
|
||||||
|
menuCallbacks,
|
||||||
|
baseUrl,
|
||||||
|
UUID,
|
||||||
|
forceQuitAndInstallUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
pgAdminMainScreen.webContents.setWindowOpenHandler(({url})=>{
|
pgAdminMainScreen.webContents.setWindowOpenHandler(({url})=>{
|
||||||
let openDocsInBrowser = configStore.get('openDocsInBrowser', true);
|
let openDocsInBrowser = configStore.get('openDocsInBrowser', true);
|
||||||
let isDocURL = false;
|
let isDocURL = false;
|
||||||
|
|
@ -377,18 +420,50 @@ function launchPgAdminWindow() {
|
||||||
});
|
});
|
||||||
|
|
||||||
pgAdminMainScreen.on('closed', ()=>{
|
pgAdminMainScreen.on('closed', ()=>{
|
||||||
|
cleanupAutoUpdateFlag();
|
||||||
misc.cleanupAndQuitApp();
|
misc.cleanupAndQuitApp();
|
||||||
});
|
});
|
||||||
|
|
||||||
pgAdminMainScreen.on('close', () => {
|
pgAdminMainScreen.on('close', () => {
|
||||||
configStore.set('bounds', pgAdminMainScreen.getBounds());
|
configStore.set('bounds', pgAdminMainScreen.getBounds());
|
||||||
|
updateConfigAndMenus('error-close', configStore, pgAdminMainScreen, menuCallbacks);
|
||||||
pgAdminMainScreen.removeAllListeners('close');
|
pgAdminMainScreen.removeAllListeners('close');
|
||||||
pgAdminMainScreen.close();
|
pgAdminMainScreen.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Notify if update was installed (fix: always check after main window is ready)
|
||||||
|
notifyUpdateInstalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
let splashWindow;
|
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.
|
// setup preload events.
|
||||||
ipcMain.handle('showOpenDialog', (e, options) => dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender), options));
|
ipcMain.handle('showOpenDialog', (e, options) => dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender), options));
|
||||||
ipcMain.handle('showSaveDialog', (e, options) => dialog.showSaveDialog(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.relaunch();
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
});
|
});
|
||||||
ipcMain.on('log', (_e, text) => ()=>{
|
ipcMain.on('log', (_e, text) => {
|
||||||
misc.writeServerLog(text);
|
misc.writeServerLog(text);
|
||||||
});
|
});
|
||||||
ipcMain.on('focus', (e) => {
|
ipcMain.on('focus', (e) => {
|
||||||
|
|
@ -428,6 +503,7 @@ ipcMain.handle('checkPortAvailable', async (_e, fixedPort)=>{
|
||||||
});
|
});
|
||||||
ipcMain.handle('openConfigure', openConfigure);
|
ipcMain.handle('openConfigure', openConfigure);
|
||||||
|
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
splashWindow = new BrowserWindow({
|
splashWindow = new BrowserWindow({
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,10 @@ contextBridge.exposeInMainWorld('electronUI', {
|
||||||
downloadStreamSaveEnd: (...args) => ipcRenderer.send('download-stream-save-end', ...args),
|
downloadStreamSaveEnd: (...args) => ipcRenderer.send('download-stream-save-end', ...args),
|
||||||
downloadBase64UrlData: (...args) => ipcRenderer.invoke('download-base64-url-data', ...args),
|
downloadBase64UrlData: (...args) => ipcRenderer.invoke('download-base64-url-data', ...args),
|
||||||
downloadTextData: (...args) => ipcRenderer.invoke('download-text-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 getApiInstance from '../../../static/js/api_instance';
|
||||||
import usePreferences, { setupPreferenceBroadcast } from '../../../preferences/static/js/store';
|
import usePreferences, { setupPreferenceBroadcast } from '../../../preferences/static/js/store';
|
||||||
import checkNodeVisibility from '../../../static/js/check_node_visibility';
|
import checkNodeVisibility from '../../../static/js/check_node_visibility';
|
||||||
|
import {appAutoUpdateNotifier} from '../../../static/js/helpers/appAutoUpdateNotifier';
|
||||||
|
|
||||||
define('pgadmin.browser', [
|
define('pgadmin.browser', [
|
||||||
'sources/gettext', 'sources/url_for', 'sources/pgadmin',
|
'sources/gettext', 'sources/url_for', 'sources/pgadmin',
|
||||||
|
|
@ -272,12 +273,34 @@ define('pgadmin.browser', [
|
||||||
checkMasterPassword(data, self.masterpass_callback_queue, cancel_callback);
|
checkMasterPassword(data, self.masterpass_callback_queue, cancel_callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
check_version_update: function() {
|
check_version_update: async function(trigger_update_check=false) {
|
||||||
getApiInstance().get(
|
getApiInstance().get(
|
||||||
url_for('misc.upgrade_check')
|
url_for('misc.upgrade_check') + '?trigger_update_check=' + trigger_update_check
|
||||||
).then((res)=> {
|
).then((res)=> {
|
||||||
const data = res.data.data;
|
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(
|
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)}
|
${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
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// If the user manually triggered a check for updates (trigger_update_check is true)
|
||||||
}).catch(function() {
|
// and no update is available (data.outdated is false), show an info notification.
|
||||||
// Suppress any errors
|
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.utils.session import cleanup_session_files
|
||||||
from pgadmin.misc.themes import get_all_themes
|
from pgadmin.misc.themes import get_all_themes
|
||||||
from pgadmin.utils.ajax import precondition_required, make_json_response, \
|
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, \
|
from pgadmin.utils.heartbeat import log_server_heartbeat, \
|
||||||
get_server_heartbeat, stop_server_heartbeat
|
get_server_heartbeat, stop_server_heartbeat
|
||||||
import config
|
import config
|
||||||
|
|
@ -32,6 +32,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import ssl
|
import ssl
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
from urllib.parse import unquote
|
||||||
from pgadmin.settings import get_setting, store_setting
|
from pgadmin.settings import get_setting, store_setting
|
||||||
|
|
||||||
MODULE_NAME = 'misc'
|
MODULE_NAME = 'misc'
|
||||||
|
|
@ -171,7 +172,7 @@ class MiscModule(PgAdminModule):
|
||||||
return ['misc.ping', 'misc.index', 'misc.cleanup',
|
return ['misc.ping', 'misc.index', 'misc.cleanup',
|
||||||
'misc.validate_binary_path', 'misc.log_heartbeat',
|
'misc.validate_binary_path', 'misc.log_heartbeat',
|
||||||
'misc.stop_heartbeat', 'misc.get_heartbeat',
|
'misc.stop_heartbeat', 'misc.get_heartbeat',
|
||||||
'misc.upgrade_check']
|
'misc.upgrade_check', 'misc.auto_update']
|
||||||
|
|
||||||
def register(self, app, options):
|
def register(self, app, options):
|
||||||
"""
|
"""
|
||||||
|
|
@ -343,19 +344,28 @@ def validate_binary_path():
|
||||||
methods=['GET'])
|
methods=['GET'])
|
||||||
@pga_login_required
|
@pga_login_required
|
||||||
def upgrade_check():
|
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.
|
Check for application updates and return update metadata to the client.
|
||||||
ret = {
|
- Compares current version with remote version data.
|
||||||
"outdated": False,
|
- 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:
|
if config.UPGRADE_CHECK_ENABLED:
|
||||||
last_check = get_setting('LastUpdateCheck', default='0')
|
last_check = get_setting('LastUpdateCheck', default='0')
|
||||||
today = time.strftime('%Y%m%d')
|
today = time.strftime('%Y%m%d')
|
||||||
if int(last_check) < int(today):
|
|
||||||
data = None
|
data = None
|
||||||
url = '%s?version=%s' % (
|
url = '%s?version=%s' % (
|
||||||
config.UPGRADE_CHECK_URL, config.APP_VERSION)
|
config.UPGRADE_CHECK_URL, config.APP_VERSION)
|
||||||
current_app.logger.debug('Checking version data at: %s' % url)
|
current_app.logger.debug('Checking version data at: %s' % url)
|
||||||
|
|
||||||
|
# Attempt to fetch upgrade data from remote URL
|
||||||
try:
|
try:
|
||||||
# Do not wait for more than 5 seconds.
|
# Do not wait for more than 5 seconds.
|
||||||
# It stuck on rendering the browser.html, while working in the
|
# It stuck on rendering the browser.html, while working in the
|
||||||
|
|
@ -384,11 +394,42 @@ def upgrade_check():
|
||||||
'Exception when checking for update')
|
'Exception when checking for update')
|
||||||
return internal_server_error('Failed to check for update')
|
return internal_server_error('Failed to check for update')
|
||||||
|
|
||||||
if data is not None and \
|
if data:
|
||||||
data[config.UPGRADE_CHECK_KEY]['version_int'] > \
|
# Determine platform
|
||||||
config.APP_VERSION_INT:
|
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 = {
|
ret = {
|
||||||
"outdated": True,
|
"outdated": True,
|
||||||
|
"check_for_auto_updates": False,
|
||||||
"current_version": config.APP_VERSION,
|
"current_version": config.APP_VERSION,
|
||||||
"upgrade_version": data[config.UPGRADE_CHECK_KEY][
|
"upgrade_version": data[config.UPGRADE_CHECK_KEY][
|
||||||
'version'],
|
'version'],
|
||||||
|
|
@ -396,6 +437,45 @@ def upgrade_check():
|
||||||
"download_url": data[config.UPGRADE_CHECK_KEY][
|
"download_url": data[config.UPGRADE_CHECK_KEY][
|
||||||
'download_url']
|
'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)
|
store_setting('LastUpdateCheck', today)
|
||||||
return make_json_response(data=ret)
|
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': [
|
'file_items': [
|
||||||
MenuItem(
|
MenuItem(
|
||||||
name='mnu_resetlayout',
|
name='mnu_resetlayout',
|
||||||
priority=998,
|
priority=997,
|
||||||
module="pgAdmin.Settings",
|
module="pgAdmin.Settings",
|
||||||
callback='show',
|
callback='show',
|
||||||
label=gettext('Reset Layout')
|
label=gettext('Reset Layout')
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import { useWorkspace, WorkspaceProvider } from '../../misc/workspaces/static/js
|
||||||
import { PgAdminProvider, usePgAdmin } from './PgAdminProvider';
|
import { PgAdminProvider, usePgAdmin } from './PgAdminProvider';
|
||||||
import PreferencesComponent from '../../preferences/static/js/components/PreferencesComponent';
|
import PreferencesComponent from '../../preferences/static/js/components/PreferencesComponent';
|
||||||
import { ApplicationStateProvider } from '../../settings/static/ApplicationStateProvider';
|
import { ApplicationStateProvider } from '../../settings/static/ApplicationStateProvider';
|
||||||
|
import { appAutoUpdateNotifier } from './helpers/appAutoUpdateNotifier';
|
||||||
|
|
||||||
const objectExplorerGroup = {
|
const objectExplorerGroup = {
|
||||||
tabLocked: true,
|
tabLocked: true,
|
||||||
|
|
@ -181,6 +181,36 @@ export default function BrowserComponent({pgAdmin}) {
|
||||||
isNewTab: true,
|
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(()=>{
|
useEffect(()=>{
|
||||||
if(uiReady) {
|
if(uiReady) {
|
||||||
pgAdmin?.Browser?.uiloaded?.();
|
pgAdmin?.Browser?.uiloaded?.();
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ export default function(basicSettings) {
|
||||||
main: '#eea236',
|
main: '#eea236',
|
||||||
light: '#fce5c5',
|
light: '#fce5c5',
|
||||||
contrastText: '#000',
|
contrastText: '#000',
|
||||||
|
hoverMain: darken('#eea236', 0.1),
|
||||||
|
hoverBorderColor: darken('#eea236', 0.1),
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
main: '#fde74c',
|
main: '#fde74c',
|
||||||
|
|
|
||||||
|
|
@ -1289,6 +1289,7 @@ const StyledNotifierMessageBox = styled(Box)(({theme}) => ({
|
||||||
backgroundColor: theme.palette.warning.light,
|
backgroundColor: theme.palette.warning.light,
|
||||||
'& .FormFooter-iconWarning': {
|
'& .FormFooter-iconWarning': {
|
||||||
color: theme.palette.warning.main,
|
color: theme.palette.warning.main,
|
||||||
|
marginBottom: theme.spacing(8),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'& .FormFooter-message': {
|
'& .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