mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-08-19 01:15:04 -05:00
Add support for automatic updates in the pgAdmin 4 Desktop application on macOS. #5766
This commit is contained in:
@@ -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}`));
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
+131
-51
@@ -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,59 +344,138 @@ 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)
|
||||
try:
|
||||
# Do not wait for more than 5 seconds.
|
||||
# It stuck on rendering the browser.html, while working in the
|
||||
# broken network.
|
||||
if os.path.exists(config.CA_FILE) and sys.version_info >= (
|
||||
3, 13):
|
||||
# Use SSL context for Python 3.13+
|
||||
context = ssl.create_default_context(cafile=config.CA_FILE)
|
||||
response = urlopen(url, data=data, timeout=5,
|
||||
context=context)
|
||||
elif os.path.exists(config.CA_FILE):
|
||||
# Use cafile parameter for older versions
|
||||
response = urlopen(url, data=data, timeout=5,
|
||||
cafile=config.CA_FILE)
|
||||
|
||||
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
|
||||
# broken network.
|
||||
if os.path.exists(config.CA_FILE) and sys.version_info >= (
|
||||
3, 13):
|
||||
# Use SSL context for Python 3.13+
|
||||
context = ssl.create_default_context(cafile=config.CA_FILE)
|
||||
response = urlopen(url, data=data, timeout=5,
|
||||
context=context)
|
||||
elif os.path.exists(config.CA_FILE):
|
||||
# Use cafile parameter for older versions
|
||||
response = urlopen(url, data=data, timeout=5,
|
||||
cafile=config.CA_FILE)
|
||||
else:
|
||||
response = urlopen(url, data, 5)
|
||||
current_app.logger.debug(
|
||||
'Version check HTTP response code: %d' % response.getcode()
|
||||
)
|
||||
|
||||
if response.getcode() == 200:
|
||||
data = json.loads(response.read().decode('utf-8'))
|
||||
current_app.logger.debug('Response data: %s' % data)
|
||||
except Exception:
|
||||
current_app.logger.exception(
|
||||
'Exception when checking for update')
|
||||
return internal_server_error('Failed to check for update')
|
||||
|
||||
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'],
|
||||
"product_name": config.APP_NAME,
|
||||
"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:
|
||||
response = urlopen(url, data, 5)
|
||||
current_app.logger.debug(
|
||||
'Version check HTTP response code: %d' % response.getcode()
|
||||
)
|
||||
|
||||
if response.getcode() == 200:
|
||||
data = json.loads(response.read().decode('utf-8'))
|
||||
current_app.logger.debug('Response data: %s' % data)
|
||||
except Exception:
|
||||
current_app.logger.exception(
|
||||
'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:
|
||||
ret = {
|
||||
"outdated": True,
|
||||
"current_version": config.APP_VERSION,
|
||||
"upgrade_version": data[config.UPGRADE_CHECK_KEY][
|
||||
'version'],
|
||||
"product_name": config.APP_NAME,
|
||||
"download_url": data[config.UPGRADE_CHECK_KEY][
|
||||
'download_url']
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user