Replace pgAdmin NW.js container with Electron container. #7494

This commit is contained in:
Aditya Toshniwal 2024-07-01 11:27:42 +05:30 committed by GitHub
parent 4457a6a6a1
commit 91eb60a363
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1459 additions and 2124 deletions

2
.gitignore vendored
View File

@ -9,6 +9,7 @@
.DS_Store
.idea
.vscode
.sonarlint
.yarn
/*.diff
/*.patch
@ -38,6 +39,7 @@ runtime/pgAdmin4.app/
runtime/pgAdmin4.pro.user*
runtime/pgAdmin4_resource.rc
runtime/release/
runtime/dev_config.json
runtime/ui_BrowserWindow.h
web/config_local.py
web/pgadmin/misc/themes/pgadmin.themes.json

View File

@ -260,45 +260,32 @@ REM Main build sequence Ends
RD /Q /S "%BUILDROOT%\docs\en_US\html\_sources" 1> nul 2>&1
ECHO Staging runtime components...
XCOPY /S /I /E /H /Y "%WD%\runtime\assets" "%BUILDROOT%\runtime\assets" > nul || EXIT /B 1
XCOPY /S /I /E /H /Y "%WD%\runtime\src" "%BUILDROOT%\runtime\src" > nul || EXIT /B 1
MKDIR "%BUILDROOT%\runtime\resources\app"
XCOPY /S /I /E /H /Y "%WD%\runtime\assets" "%BUILDROOT%\runtime\resources\app\assets" > nul || EXIT /B 1
XCOPY /S /I /E /H /Y "%WD%\runtime\src" "%BUILDROOT%\runtime\resources\app\src" > nul || EXIT /B 1
COPY "%WD%\runtime\package.json" "%BUILDROOT%\runtime\" > nul || EXIT /B 1
CD "%BUILDROOT%\runtime\"
COPY "%WD%\runtime\package.json" "%BUILDROOT%\runtime\resources\app\" > nul || EXIT /B 1
CD "%BUILDROOT%\runtime\resources\app\"
CALL yarn install --production=true || EXIT /B 1
ECHO Downloading NWjs to %TMPDIR%...
REM Get a fresh copy of nwjs.
REM NOTE: The nw download servers seem to be very unreliable, so at the moment we're using wget which retries
REM YARN
REM CALL yarn --cwd "%TMPDIR%" add nw || EXIT /B
REM YARN END
ECHO Downloading Electron to %TMPDIR%...
REM Get a fresh copy of electron.
REM WGET
REM Comment out the below line as the latest version having some
REM problem https://github.com/nwjs/nw.js/issues/7964, so for the time being
REM hardcoded the version to 0.77.0
REM FOR /f "tokens=2 delims='" %%i IN ('yarn info nw ^| findstr "latest: "') DO SET "NW_VERSION=%%i"
REM :GET_NW
REM wget https://dl.nwjs.io/v%NW_VERSION%/nwjs-v%NW_VERSION%-win-x64.zip -O "%TMPDIR%\nwjs-v%NW_VERSION%-win-x64.zip"
REM IF %ERRORLEVEL% NEQ 0 GOTO GET_NW
FOR /f "tokens=2 delims='" %%i IN ('yarn info electron ^| findstr "latest: "') DO SET "ELECTRON_VERSION=%%i"
:GET_NW
wget https://github.com/electron/electron/releases/download/v%ELECTRON_VERSION%/electron-v%ELECTRON_VERSION%-win32-x64.zip -O "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64.zip"
IF %ERRORLEVEL% NEQ 0 GOTO GET_NW
SET "NW_VERSION=0.77.0"
wget https://dl.nwjs.io/v%NW_VERSION%/nwjs-v%NW_VERSION%-win-x64.zip -O "%TMPDIR%\nwjs-v%NW_VERSION%-win-x64.zip"
tar -C "%TMPDIR%" -xvf "%TMPDIR%\nwjs-v%NW_VERSION%-win-x64.zip" || EXIT /B 1
MKDIR "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64" || EXIT /B 1
tar -C "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64" -xvf "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64.zip" || EXIT /B 1
REM WGET END
REM YARN
REM XCOPY /S /I /E /H /Y "%TMPDIR%\node_modules\nw\nwjs\*" "%BUILDROOT%\runtime" > nul || EXIT /B 1
REM YARN END
REM XCOPY
XCOPY /S /I /E /H /Y "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64\*" "%BUILDROOT%\runtime" > nul || EXIT /B 1
REM XCOPY END
REM WGET
XCOPY /S /I /E /H /Y "%TMPDIR%\nwjs-v%NW_VERSION%-win-x64\*" "%BUILDROOT%\runtime" > nul || EXIT /B 1
REM WGET END
MOVE "%BUILDROOT%\runtime\nw.exe" "%BUILDROOT%\runtime\pgAdmin4.exe"
MOVE "%BUILDROOT%\runtime\electron.exe" "%BUILDROOT%\runtime\pgAdmin4.exe"
ECHO Attempting to sign the pgAdmin4.exe...
CALL "%PGADMIN_SIGNTOOL_DIR%\signtool.exe" sign /tr http://timestamp.digicert.com "%BUILDROOT%\runtime\pgAdmin4.exe"
IF %ERRORLEVEL% NEQ 0 (
@ -310,8 +297,13 @@ REM Main build sequence Ends
)
ECHO Replacing executable icon...
CALL yarn --cwd "%TMPDIR%" add winresourcer || EXIT /B
"%TMPDIR%\node_modules\winresourcer\bin\Resourcer.exe" -op:upd -src:"%BUILDROOT%\runtime\pgAdmin4.exe" -type:Icongroup -name:IDR_MAINFRAME -file:"%WD%\pkg\win32\Resources\pgAdmin4.ico"
ECHO Downloading rcedit.exe...
wget https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe -O "%TMPDIR%\rcedit-x64.exe"
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-icon "%WD%\pkg\win32\Resources\pgAdmin4.ico"
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-version-string "FileDescription" "%APP_NAME%"
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-version-string "ProductName" "%APP_NAME%"
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-product-version "%APP_VERSION%""
ECHO Staging PostgreSQL components...
COPY "%PGADMIN_POSTGRES_DIR%\bin\libpq.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1

View File

@ -133,55 +133,47 @@ _create_python_virtualenv() {
_build_runtime() {
echo "Assembling the desktop runtime..."
# Get a fresh copy of nwjs.
# NOTE: The nw download servers seem to be very unreliable, so at the moment we're using wget
# in a retry loop as Yarn/Npm don't seem to like that.
# Get a fresh copy of electron.
# YARN:
# yarn add --cwd "${BUILDROOT}" nw
# YARN END
ELECTRON_ARCH="x64"
if [ "$(uname -m)" == "arm64" ]; then
ELECTRON_ARCH="arm64"
fi
# WGET:
# Comment out the below line as the latest version having some
# problem https://github.com/nwjs/nw.js/issues/7964, so for the time being
# hardcoded the version to 0.77.0
# NW_VERSION=$(yarn info nw | grep latest | awk -F "'" '{ print $2}')
NW_VERSION="0.77.0"
ELECTRON_VERSION=$(yarn info electron | grep latest | awk -F "'" '{ print $2}')
pushd "${BUILDROOT}" > /dev/null || exit
while true;do
wget "https://dl.nwjs.io/v${NW_VERSION}/nwjs-v${NW_VERSION}-linux-x64.tar.gz" && break
rm "nwjs-v${NW_VERSION}-linux-x64.tar.gz"
wget "https://github.com/electron/electron/releases/download/v${ELECTRON_VERSION}/electron-v${ELECTRON_VERSION}-linux-${ELECTRON_ARCH}.zip" && break
rm "electron-v${ELECTRON_VERSION}-linux-${ELECTRON_ARCH}.zip"
done
tar -zxvf "nwjs-v${NW_VERSION}-linux-x64.tar.gz"
unzip "electron-v${ELECTRON_VERSION}-linux-${ELECTRON_ARCH}.zip" -d "electron-v${ELECTRON_VERSION}-linux-${ELECTRON_ARCH}"
popd > /dev/null || exit
# WGET END
# Copy nwjs into the staging directory
# Copy electron into the staging directory
mkdir -p "${DESKTOPROOT}/usr/${APP_NAME}/bin"
# The chmod command below is needed to fix the permission issue of
# the NWjs binaries and files.
# Change the permission for others and group the same as the owner
chmod -R og=u "${BUILDROOT}/nwjs-v${NW_VERSION}-linux-x64"/*
chmod -R og=u "${BUILDROOT}/electron-v${ELECTRON_VERSION}-linux-${ELECTRON_ARCH}"/*
# Explicitly remove write permissions for others and group
chmod -R og-w "${BUILDROOT}/nwjs-v${NW_VERSION}-linux-x64"/*
chmod -R og-w "${BUILDROOT}/electron-v${ELECTRON_VERSION}-linux-${ELECTRON_ARCH}"/*
# YARN:
# cp -r "${BUILDROOT}/node_modules/nw/nwjs"/* "${DESKTOPROOT}/usr/${APP_NAME}/bin"
# YARN END
BUNDLEDIR="${DESKTOPROOT}/usr/${APP_NAME}/bin"
# WGET:
cp -r "${BUILDROOT}/nwjs-v${NW_VERSION}-linux-x64"/* "${DESKTOPROOT}/usr/${APP_NAME}/bin"
cp -r "${BUILDROOT}/electron-v${ELECTRON_VERSION}-linux-${ELECTRON_ARCH}"/* "${BUNDLEDIR}"
# WGET END
mv "${DESKTOPROOT}/usr/${APP_NAME}/bin/nw" "${DESKTOPROOT}/usr/${APP_NAME}/bin/${APP_NAME}"
mv "${BUNDLEDIR}/electron" "${BUNDLEDIR}/${APP_NAME}"
cp -r "${SOURCEDIR}/runtime/assets" "${DESKTOPROOT}/usr/${APP_NAME}/bin/assets"
cp -r "${SOURCEDIR}/runtime/src" "${DESKTOPROOT}/usr/${APP_NAME}/bin/src"
mkdir -p "${BUNDLEDIR}/resources/app"
cp -r "${SOURCEDIR}/runtime/assets" "${BUNDLEDIR}/resources/app/assets"
cp -r "${SOURCEDIR}/runtime/src" "${BUNDLEDIR}/resources/app/src"
cp "${SOURCEDIR}/runtime/package.json" "${DESKTOPROOT}/usr/${APP_NAME}/bin/"
yarn --cwd "${DESKTOPROOT}/usr/${APP_NAME}/bin" install --production=true
cp "${SOURCEDIR}/runtime/package.json" "${BUNDLEDIR}/resources/app"
yarn --cwd "${BUNDLEDIR}/resources/app" install --production=true
# Create the icon
mkdir -p "${DESKTOPROOT}/usr/share/icons/hicolor/128x128/apps/"

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>%APPID%</string>
<key>CFBundleName</key>
<string>%APPNAME%</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTSDKBuild</key>
<string>23A334</string>
<key>DTSDKName</key>
<string>macosx14.0</string>
<key>DTXcode</key>
<string>1501</string>
<key>DTXcodeBuild</key>
<string>15A507</string>
<key>LSEnvironment</key>
<dict>
<key>MallocNanoZone</key>
<string>0</string>
</dict>
<key>LSUIElement</key>
<true/>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>CFBundleDisplayName</key>
<string>%APPNAME%</string>
<key>CFBundleExecutable</key>
<string>%APPNAME%</string>
<key>CFBundleShortVersionString</key>
<string>%APPVER%</string>
</dict>
</plist>

View File

@ -26,56 +26,37 @@ _build_runtime() {
echo "Assembling the runtime environment..."
test -d "${BUILD_ROOT}" || mkdir "${BUILD_ROOT}"
# Get a fresh copy of nwjs.
# NOTE: The nw download servers seem to be very unreliable, so at the moment we're using wget
# in a retry loop as Yarn/Npm don't seem to like that.
# Get a fresh copy of electron
# uname -m returns "x86_64" on Intel, but we need "x64"
NW_ARCH="x64"
ELECTRON_ARCH="x64"
if [ "$(uname -m)" == "arm64" ]; then
NW_ARCH="arm64"
ELECTRON_ARCH="arm64"
fi
# YARN:
# yarn add --cwd "${BUILDROOT}" nw
# YARN END
# WGET:
# Comment out the below line as the latest version having some
# problem https://github.com/nwjs/nw.js/issues/7964, so for the time being
# hardcoded the version to 0.77.0
# NW_VERSION=$(yarn info nw | grep latest | awk -F "'" '{ print $2}')
NW_VERSION="0.77.0"
ELECTRON_VERSION=$(yarn info electron | grep latest | awk -F "'" '{ print $2}')
pushd "${BUILD_ROOT}" > /dev/null || exit
while true;do
wget "https://dl.nwjs.io/v${NW_VERSION}/nwjs-v${NW_VERSION}-osx-${NW_ARCH}.zip" && break
rm "nwjs-v${NW_VERSION}-osx-${NW_ARCH}.zip"
wget "https://github.com/electron/electron/releases/download/v${ELECTRON_VERSION}/electron-v${ELECTRON_VERSION}-darwin-${ELECTRON_ARCH}.zip" && break
rm "electron-v${ELECTRON_VERSION}-darwin-${ELECTRON_ARCH}.zip"
done
unzip "nwjs-v${NW_VERSION}-osx-${NW_ARCH}.zip"
unzip "electron-v${ELECTRON_VERSION}-darwin-${ELECTRON_ARCH}.zip"
popd > /dev/null || exit
# WGET END
# YARN:
# cp -R "${BUILD_ROOT}/node_modules/nw/nwjs/nwjs.app" "${BUILD_ROOT}/"
# YARN END
# WGET:
cp -R "${BUILD_ROOT}/nwjs-v${NW_VERSION}-osx-${NW_ARCH}"/nwjs.app "${BUILD_ROOT}/"
# WGET END
mv "${BUILD_ROOT}/nwjs.app" "${BUNDLE_DIR}"
mv "${BUILD_ROOT}/Electron.app" "${BUNDLE_DIR}"
find "${BUNDLE_DIR}" -exec touch {} \;
# Copy in the runtime code
mkdir "${BUNDLE_DIR}/Contents/Resources/app.nw/"
cp -R "${SOURCE_DIR}/runtime/assets" "${BUNDLE_DIR}/Contents/Resources/app.nw/"
cp -R "${SOURCE_DIR}/runtime/src" "${BUNDLE_DIR}/Contents/Resources/app.nw/"
cp "${SOURCE_DIR}/runtime/package.json" "${BUNDLE_DIR}/Contents/Resources/app.nw/"
cp "${SOURCE_DIR}/runtime/.yarnrc.yml" "${BUNDLE_DIR}/Contents/Resources/app.nw/"
mkdir "${BUNDLE_DIR}/Contents/Resources/app/"
cp -R "${SOURCE_DIR}/runtime/assets" "${BUNDLE_DIR}/Contents/Resources/app/"
cp -R "${SOURCE_DIR}/runtime/src" "${BUNDLE_DIR}/Contents/Resources/app/"
cp "${SOURCE_DIR}/runtime/package.json" "${BUNDLE_DIR}/Contents/Resources/app/"
cp "${SOURCE_DIR}/runtime/.yarnrc.yml" "${BUNDLE_DIR}/Contents/Resources/app/"
# Install the runtime node_modules, then replace the package.json
pushd "${BUNDLE_DIR}/Contents/Resources/app.nw/" > /dev/null || exit
pushd "${BUNDLE_DIR}/Contents/Resources/app/" > /dev/null || exit
yarn set version berry
yarn set version 3
yarn plugin import workspace-tools
@ -178,7 +159,6 @@ _fixup_imports() {
# Find all the files that may need tweaks
TODO=$(find . -perm +0111 -type f -exec file "{}" \; | \
grep -v "Frameworks/Python.framework" | \
grep -v "Frameworks/nwjs" | \
grep -E "Mach-O 64-bit" | \
awk -F ':| ' '{ORS=" "; print $1}' | \
uniq)
@ -273,10 +253,19 @@ _complete_bundle() {
sed -i '' "s/%APPNAME%/${APP_NAME}/g" "${BUNDLE_DIR}/Contents/Info.plist"
sed -i '' "s/%APPVER%/${APP_LONG_VERSION}/g" "${BUNDLE_DIR}/Contents/Info.plist"
sed -i '' "s/%APPID%/org.pgadmin.pgadmin4/g" "${BUNDLE_DIR}/Contents/Info.plist"
for FILE in "${BUNDLE_DIR}"/Contents/Resources/*.lproj/InfoPlist.strings; do
sed -i '' 's/CFBundleGetInfoString =.*/CFBundleGetInfoString = "Copyright (C) 2013 - 2024, The pgAdmin Development Team";/g' "${FILE}"
sed -i '' 's/NSHumanReadableCopyright =.*/NSHumanReadableCopyright = "Copyright (C) 2013 - 2024, The pgAdmin Development Team";/g' "${FILE}"
echo CFBundleDisplayName = \""${APP_NAME}"\"\; >> "${FILE}"
# Rename helper execs and Update the plist
for helper_exec in "Electron Helper" "Electron Helper (Renderer)" "Electron Helper (Plugin)" "Electron Helper (GPU)"
do
pgadmin_exec=${helper_exec//Electron/pgAdmin 4}
mv "${BUNDLE_DIR}/Contents/Frameworks/${helper_exec}.app/Contents/MacOS/${helper_exec}" "${BUNDLE_DIR}/Contents/Frameworks/${helper_exec}.app/Contents/MacOS/${pgadmin_exec}"
mv "${BUNDLE_DIR}/Contents/Frameworks/${helper_exec}.app" "${BUNDLE_DIR}/Contents/Frameworks/${pgadmin_exec}.app"
info_plist="${BUNDLE_DIR}/Contents/Frameworks/${pgadmin_exec}.app/Contents/Info.plist"
cp Info.plist-helper.in "${info_plist}"
sed -i '' "s/%APPNAME%/${pgadmin_exec}/g" "${info_plist}"
sed -i '' "s/%APPVER%/${APP_LONG_VERSION}/g" "${info_plist}"
sed -i '' "s/%APPID%/org.pgadmin.pgadmin4.helper/g" "${info_plist}"
done
# PkgInfo
@ -286,10 +275,10 @@ _complete_bundle() {
cp pgAdmin4.icns "${BUNDLE_DIR}/Contents/Resources/app.icns"
# Rename the executable
mv "${BUNDLE_DIR}/Contents/MacOS/nwjs" "${BUNDLE_DIR}/Contents/MacOS/${APP_NAME}"
mv "${BUNDLE_DIR}/Contents/MacOS/Electron" "${BUNDLE_DIR}/Contents/MacOS/${APP_NAME}"
# Rename the app in package.json so the menu looks as it should
sed -i '' "s/\"name\": \"pgadmin4\"/\"name\": \"${APP_NAME}\"/g" "${BUNDLE_DIR}/Contents/Resources/app.nw/package.json"
sed -i '' "s/\"name\": \"pgadmin4\"/\"name\": \"${APP_NAME}\"/g" "${BUNDLE_DIR}/Contents/Resources/app/package.json"
# Import the dependencies, and rewrite any library references
_fixup_imports "${BUNDLE_DIR}"

View File

@ -41,7 +41,6 @@ sshtunnel==0.*
ldap3==2.*
gssapi==1.8.*
eventlet==0.36.1
httpagentparser==1.9.*
user-agents==2.2.0
pywinpty==2.0.*; sys_platform=="win32"
Authlib==1.3.*; python_version > '3.7'

View File

@ -1,6 +0,0 @@
generated
node_modules
vendor
templates/
templates\
ycache

View File

@ -6,47 +6,55 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import globals from 'globals';
import js from '@eslint/js';
module.exports = {
'env': {
'browser': true,
'es6': true,
'amd': true,
},
'extends': [
'eslint:recommended',
],
'parserOptions': {
'ecmaVersion': 2018,
"sourceType": "module",
},
'globals': {
'_': true,
'module': true,
'process': true,
'nw': true,
'platform': true
},
'rules': {
'indent': [
'error',
2
export default [
js.configs.recommended,
{
files: ['**/*.js'],
ignores: [
'generated',
'node_modules',
'vendor',
'templates/',
'templates\\',
'ycache',
],
'linebreak-style': 0,
'quotes': [
'error',
'single'
],
'semi': [
'error',
'always'
],
'comma-dangle': [
'error',
'always-multiline'
],
'no-console': ["error", { allow: ["warn", "error"] }],
// We need to exclude below for RegEx case
"no-useless-escape": 0,
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.browser,
...globals.es2017,
...globals.amd,
'_': 'readonly',
'module': 'readonly',
'process': 'readonly',
'platform': 'readonly',
},
},
'rules': {
'indent': [
'error',
2,
],
'linebreak-style': 0,
'quotes': [
'error',
'single',
],
'semi': [
'error',
'always',
],
'comma-dangle': [
'error',
'always-multiline',
],
'no-console': ['error', { allow: ['warn', 'error'] }],
// We need to exclude below for RegEx case
'no-useless-escape': 0,
},
},
};
];

View File

@ -1,37 +1,24 @@
{
"name": "pgadmin4",
"description": "pgAdmin is the most popular and feature rich Open Source administration and development platform for PostgreSQL, the most advanced Open Source database in the world.",
"main": "src/html/pgadmin.html",
"repository": "https://github.com/postgres/pgadmin4.git",
"author": "pgAdmin Development Team (https://www.pgadmin.org/)",
"license": "PostgreSQL",
"chromium-args": "--disable-popup-blocking --disable-gpu --disable-devtools",
"user-agent": "Nwjs:%nwver-%osinfo-%chromium_ver",
"nodejs": true,
"window": {
"width": 750,
"height": 600,
"toolbar": false,
"fullscreen": false,
"frame": false,
"show": true,
"position": "center",
"always-on-top": true,
"icon": "assets/pgAdmin4.png"
"type": "module",
"main": "src/js/pgadmin.js",
"scripts": {
"start": "electron .",
"linter": "yarn run eslint -c .eslintrc.js ."
},
"icons": {
"256": "assets/pgAdmin4.png"
"packageManager": "yarn@3.8.2",
"devDependencies": {
"@eslint/js": "^9.5.0",
"electron": "^30.0.5",
"eslint": "^9.5.0"
},
"dependencies": {
"axios": "^1.7.2",
"bootstrap": "^4.5.3"
},
"devDependencies": {
"eslint": "^9.5.0",
"nw": "0.77.0"
},
"scripts": {
"linter": "yarn eslint --no-eslintrc -c .eslintrc.js --ext .js ."
},
"packageManager": "yarn@3.8.2"
"bootstrap": "^4.5.3",
"electron-store": "^9.0.0"
}
}

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>pgAdmin 4 Configuration</title>
<title>pgAdmin 4 Runtime Configuration</title>
<link rel="stylesheet" href="../css/pgadmin-desktop.css"/>
<style>
body, html {
@ -76,6 +76,99 @@
<button id="btnSave" type="submit" class="btn btn-primary" disabled>Save</button>
</div>
</div>
<script type="text/javascript" src="../js/configure.js"></script>
<script type="text/javascript" >
let configData;
async function checkConfiguration() {
const portNo = configData.portNo;
if (document.getElementById('fixedPortCheck').checked && portNo !== document.getElementById('portNo').value) {
let fixedPort = parseInt(document.getElementById('portNo').value);
// get the available TCP port
if(await window.electronUI.checkPortAvailable(fixedPort)) {
saveConfiguration();
} else {
alert('The port specified is already in use. Please enter a free port number.');
}
} else {
saveConfiguration();
}
}
async function saveConfiguration() {
await window.electronUI.setConfigData({
'fixedPort': document.getElementById('fixedPortCheck').checked,
'portNo': parseInt(document.getElementById('portNo').value),
'connectionTimeout': parseInt(document.getElementById('timeOut').value),
'openDocsInBrowser': document.getElementById('openDocsInBrowser').checked
});
document.getElementById('status-text').innerHTML = 'Configuration Saved';
const result = await window.electronUI.showMessageBox({
'type': 'question',
'title': 'Confirmation',
'message': "pgAdmin 4 must be restarted for changes to take effect.\n\n Do you want to quit the application?",
'buttons': [
'Yes',
'No'
]
});
if(result.response === 0) {
window.electronUI.restartApp();
}
window.close();
}
function onCheckChange() {
if (this.checked) {
document.getElementById('portNo').removeAttribute('disabled');
} else {
document.getElementById('portNo').setAttribute('disabled', 'disabled');
}
// Enable/Disable Save button
enableDisableSaveButton();
}
function enableDisableSaveButton() {
if (configData['fixedPort'] !== document.getElementById('fixedPortCheck').checked ||
configData['portNo'] != document.getElementById('portNo').value ||
configData['connectionTimeout'] != document.getElementById('timeOut').value ||
configData['openDocsInBrowser'] !== document.getElementById('openDocsInBrowser').checked) {
document.getElementById('btnSave').removeAttribute('disabled');
} else {
document.getElementById('btnSave').setAttribute('disabled', 'disabled');
}
}
window.onload = async function() {
configData = await window.electronUI.getConfigData();
document.getElementById('status-text').innerHTML = '';
// Set the GUI value as per configuration.
if (configData['fixedPort']) {
document.getElementById('fixedPortCheck').checked = true;
document.getElementById('portNo').disabled = false;
} else {
document.getElementById('fixedPortCheck').checked = false;
document.getElementById('portNo').disabled = true;
}
document.getElementById('portNo').value = configData['portNo'];
document.getElementById('timeOut').value = configData['connectionTimeout'];
if (configData['openDocsInBrowser']) {
document.getElementById('openDocsInBrowser').checked = true;
} else {
document.getElementById('openDocsInBrowser').checked = false;
}
// Add event listeners
document.getElementById('btnSave').addEventListener('click', checkConfiguration);
document.getElementById('fixedPortCheck').addEventListener('change', onCheckChange);
document.getElementById('portNo').addEventListener('change', enableDisableSaveButton);
document.getElementById('timeOut').addEventListener('change', enableDisableSaveButton);
document.getElementById('openDocsInBrowser').addEventListener('change', enableDisableSaveButton);
};
</script>
</body>
</html>

View File

@ -31,6 +31,14 @@
</div>
</div>
</div>
<script type="text/javascript" src="../js/server_error.js"></script>
<script>
window.onload = async function() {
document.getElementById('server_error_label').innerHTML = 'The pgAdmin 4 server could not be contacted:';
document.getElementById('server_error_log').innerHTML = await window.electronUI.readServerLog();
document.getElementById('btnConfigure').addEventListener('click', function() {
window.electronUI.openConfigure();
});
}
</script>
</body>
</html>

View File

@ -19,6 +19,7 @@
background-size: contain;
overflow: hidden;
display: flex;
-webkit-app-region: drag;
}
.loader-text {
@ -50,9 +51,7 @@
</head>
<body>
<div class="bg">
<span id="loader-text-status" class="loader-text"></span>
<span id="loader-text-status" class="loader-text">Waiting for pgAdmin 4 to start...</span>
</div>
<script type="text/javascript" src="../js/pgadmin.js"></script>
</body>
</html>

View File

@ -31,6 +31,19 @@
</div>
</div>
</div>
<script type="text/javascript" src="../js/view_log.js"></script>
<script>
window.onload = async function() {
document.getElementById('status-text').innerHTML = '';
document.getElementById('server_log_label').innerHTML = 'Server Log: ' + '(' + await window.electronUI.getServerLogFile() + ')';
document.getElementById('server_log').innerHTML = await window.electronUI.readServerLog();
document.getElementById('btnReload').addEventListener('click', function() {
document.getElementById('server_log').innerHTML = 'Loading logs...';
setTimeout(async function() {
document.getElementById('server_log').innerHTML = await window.electronUI.readServerLog();
}, 500);
document.getElementById('status-text').innerHTML = 'Logs reloaded successfully';
});
}
</script>
</body>
</html>

View File

@ -1,101 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
const misc = require('../js/misc.js');
// Get the window object of view log window
let gui = require('nw.gui');
let configWindow = gui.Window.get();
function checkConfiguration() {
let configData = misc.ConfigureStore.getConfigData();
if (document.getElementById('fixedPortCheck').checked && configData['portNo'] !== document.getElementById('portNo').value) {
let fixedPort = parseInt(document.getElementById('portNo').value);
// get the available TCP port
misc.getAvailablePort(fixedPort)
.then(() => {
saveConfiguration();
})
.catch(() => {
alert('The port specified is already in use. Please enter a free port number.');
});
} else {
saveConfiguration();
}
}
function saveConfiguration() {
misc.ConfigureStore.set('fixedPort', document.getElementById('fixedPortCheck').checked);
misc.ConfigureStore.set('portNo', parseInt(document.getElementById('portNo').value));
misc.ConfigureStore.set('connectionTimeout', parseInt(document.getElementById('timeOut').value));
misc.ConfigureStore.set('openDocsInBrowser', document.getElementById('openDocsInBrowser').checked);
misc.ConfigureStore.saveConfig();
document.getElementById('status-text').innerHTML = 'Configuration Saved';
if (confirm('pgAdmin 4 must be restarted for changes to take effect.\n\n Do you want to quit the application?') === true) {
misc.cleanupAndQuitApp();
}
configWindow.close();
}
function onCheckChange() {
if (this.checked) {
document.getElementById('portNo').removeAttribute('disabled');
} else {
document.getElementById('portNo').setAttribute('disabled', 'disabled');
}
// Enable/Disable Save button
enableDisableSaveButton();
}
function enableDisableSaveButton() {
let configData = misc.ConfigureStore.getConfigData();
if (configData['fixedPort'] !== document.getElementById('fixedPortCheck').checked ||
configData['portNo'] != document.getElementById('portNo').value ||
configData['connectionTimeout'] != document.getElementById('timeOut').value ||
configData['openDocsInBrowser'] !== document.getElementById('openDocsInBrowser').checked) {
document.getElementById('btnSave').removeAttribute('disabled');
} else {
document.getElementById('btnSave').setAttribute('disabled', 'disabled');
}
}
configWindow.on('loaded', function() {
document.getElementById('status-text').innerHTML = '';
// Get the config data from the file.
let configData = misc.ConfigureStore.getConfigData();
// Set the GUI value as per configuration.
if (configData['fixedPort']) {
document.getElementById('fixedPortCheck').checked = true;
document.getElementById('portNo').disabled = false;
} else {
document.getElementById('fixedPortCheck').checked = false;
document.getElementById('portNo').disabled = true;
}
document.getElementById('portNo').value = configData['portNo'];
document.getElementById('timeOut').value = configData['connectionTimeout'];
if (configData['openDocsInBrowser']) {
document.getElementById('openDocsInBrowser').checked = true;
} else {
document.getElementById('openDocsInBrowser').checked = false;
}
// Add event listeners
document.getElementById('btnSave').addEventListener('click', checkConfiguration);
document.getElementById('fixedPortCheck').addEventListener('change', onCheckChange);
document.getElementById('portNo').addEventListener('change', enableDisableSaveButton);
document.getElementById('timeOut').addEventListener('change', enableDisableSaveButton);
document.getElementById('openDocsInBrowser').addEventListener('change', enableDisableSaveButton);
});

109
runtime/src/js/menu.js Normal file
View File

@ -0,0 +1,109 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { app, Menu, ipcMain } from 'electron';
const isMac = process.platform == 'darwin';
let mainMenu;
function buildMenu(pgadminMenus, pgAdminMainScreen, callbacks) {
const template = [];
// bind all menus click event.
pgadminMenus = pgadminMenus.map((menuItem)=>{
return {
...menuItem,
submenu: menuItem.submenu?.map((subMenuItem)=>{
const smName = `${menuItem.name}_${subMenuItem.name}`;
return {
...subMenuItem,
click: ()=>{
pgAdminMainScreen.webContents.send('menu-click', smName);
},
submenu: subMenuItem.submenu?.map((deeperSubMenuItem)=>{
return {
...deeperSubMenuItem,
click: ()=>{
pgAdminMainScreen.webContents.send('menu-click', `${smName}_${deeperSubMenuItem.name}`);
},
};
}),
};
}),
};
});
let menuFile = pgadminMenus.shift();
template.push({
...menuFile,
submenu: [
...menuFile.submenu,
{ type: 'separator' },
{
label: 'View Logs...', click: callbacks['view_logs'],
},
{
label: 'Configure runtime...', click: callbacks['configure'],
},
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
});
if(isMac) {
template[0].label = app.name;
}
// push all except help
template.push(...pgadminMenus.slice(0, -1));
template.push(
{ role: 'editMenu' },
{
label: 'View',
submenu: [
{ label: 'Reload', click: ()=>pgAdminMainScreen.webContents.reload()},
{ label: 'Toggle Developer Tools', click: ()=>pgAdminMainScreen.webContents.openDevTools({ mode: 'bottom' })},
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
],
},
{ role: 'windowMenu' },
);
template.push(pgadminMenus[pgadminMenus.length-1]);
return Menu.buildFromTemplate(template);
}
export function setupMenu(pgAdminMainScreen, callbacks={}) {
ipcMain.on('setMenus', (event, menus)=>{
mainMenu = buildMenu(menus, pgAdminMainScreen, callbacks);
if(isMac) {
Menu.setApplicationMenu(mainMenu);
} else {
pgAdminMainScreen.setMenu(mainMenu);
}
ipcMain.on('enable-disable-menu-items', (event, menu, menuItem)=>{
const menuItemObj = mainMenu.getMenuItemById(menuItem?.id);
if(menuItemObj) {
menuItemObj.enabled = menuItem.isDisabled;
}
});
});
}

View File

@ -7,16 +7,13 @@
//
//////////////////////////////////////////////////////////////
const fs = require('fs');
const path = require('path');
const net = require('net');
const {platform, homedir} = require('os');
import fs from 'fs';
import path from 'path';
import net from 'net';
import {platform} from 'os';
import { app } from 'electron';
let pgadminServerProcess = null;
let pgAdminWindowObject = null;
let zoomInShortcut = null;
let zoomOutShortcut = null;
let actualSizeShortcut = null;
let toggleFullScreenShortcut = null;
// This function is used to check whether directory is present or not
// if not present then create it recursively
@ -28,26 +25,31 @@ const createDir = (dirName) => {
const insideFlatpak = () => {
return platform() === 'linux' && fs.existsSync('/.flatpak-info');
}
};
// This function is used to get the python executable path
// based on the platform. Use this for deployment.
const getPythonPath = () => {
let pythonPath;
export const getAppPaths = (basePath) => {
let pythonPath, pgadminFile;
switch (platform()) {
case 'win32':
pythonPath = '../python/python.exe';
pythonPath = '../../../../../python/python.exe';
pgadminFile = '../../../../../web/pgAdmin4.py';
break;
case 'darwin':
pythonPath = '../../Frameworks/Python.framework/Versions/Current/bin/python3';
pythonPath = '../../../../Frameworks/Python.framework/Versions/Current/bin/python3';
pgadminFile = '../../../web/pgAdmin4.py';
break;
case 'linux':
pythonPath = '../venv/bin/python3';
pythonPath = '../../../../../venv/bin/python3';
pgadminFile = '../../../../../web/pgAdmin4.py';
if (insideFlatpak()) {
pythonPath = '/usr/bin/python';
pgadminFile = '/app/pgAdmin4/web/pgAdmin4.py';
}
break;
default:
pgadminFile = '../../../web/pgAdmin4.py';
if (platform().startsWith('win')) {
pythonPath = '../python/python.exe';
} else {
@ -55,69 +57,15 @@ const getPythonPath = () => {
}
}
return pythonPath;
};
// This function is used to get the [roaming] app data path
// based on the platform. Use this for config etc.
const getAppDataPath = () => {
let appDataPath;
switch (platform()) {
case 'win32':
appDataPath = path.join(process.env.APPDATA, 'pgadmin');
break;
case 'darwin':
appDataPath = path.join(homedir(), 'Library', 'Preferences', 'pgadmin');
break;
case 'linux':
if ('XDG_CONFIG_HOME' in process.env) {
appDataPath = path.join(process.env.XDG_CONFIG_HOME, 'pgadmin');
} else {
appDataPath = path.join(homedir(), '.config', 'pgadmin');
}
break;
default:
if (platform().startsWith('win')) {
appDataPath = path.join(process.env.APPDATA, 'pgadmin');
} else if ('XDG_CONFIG_HOME' in process.env) {
appDataPath = path.join(process.env.XDG_CONFIG_HOME, 'pgadmin');
} else {
appDataPath = path.join(homedir(), '.config', 'pgadmin');
}
}
// Create directory if not exists
createDir(appDataPath);
return appDataPath;
return [path.join(basePath, pythonPath), path.join(basePath, pgadminFile)];
};
// This function is used to get the [local] app data path
// based on the platform. Use this for logs etc.
const getLocalAppDataPath = () => {
let localAppDataPath;
switch (platform()) {
case 'win32':
localAppDataPath = path.join(process.env.LOCALAPPDATA, 'pgadmin');
break;
case 'darwin':
localAppDataPath = path.join(homedir(), 'Library', 'Application Support', 'pgadmin');
break;
case 'linux':
if ('XDG_DATA_HOME' in process.env) {
localAppDataPath = path.join(process.env.XDG_DATA_HOME, 'pgadmin');
} else {
localAppDataPath = path.join(homedir(), '.local', 'share', 'pgadmin');
}
break;
default:
if (platform().startsWith('win')) {
localAppDataPath = path.join(process.env.LOCALAPPDATA, 'pgadmin');
} else if ('XDG_DATA_HOME' in process.env) {
localAppDataPath = path.join(process.env.XDG_DATA_HOME, 'pgadmin');
} else {
localAppDataPath = path.join(homedir(), '.local', 'share', 'pgadmin');
}
let localAppDataPath = app.getPath('userData');
if(process.platform == 'linux' && 'XDG_DATA_HOME' in process.env) {
localAppDataPath = path.join(process.env.XDG_DATA_HOME, app.name);
}
// Create directory if not exists
@ -128,14 +76,14 @@ const getLocalAppDataPath = () => {
// This function is used to get the random available TCP port
// if fixedPort is set to 0. Else check whether port is in used or not.
const getAvailablePort = (fixedPort) => {
export const getAvailablePort = (fixedPort) => {
return new Promise(function(resolve, reject) {
const server = net.createServer();
server.listen(fixedPort, '127.0.0.1');
server.on('error', (e) => {
reject(e.code);
reject(e instanceof Error ? e : new Error(e.code));
});
server.on('listening', () => {
@ -147,13 +95,10 @@ const getAvailablePort = (fixedPort) => {
};
// Get the app data folder path
const currentTime = (new Date()).getTime();
const serverLogFile = path.join(getLocalAppDataPath(), 'pgadmin4.' + currentTime.toString() + '.log');
const configFileName = path.join(getAppDataPath(), 'runtime_config.json');
const DEFAULT_CONFIG_DATA = {'fixedPort': false, 'portNo': 5050, 'connectionTimeout': 90, 'zoomLevel': 0, 'openDocsInBrowser': true};
const serverLogFile = path.join(getLocalAppDataPath(), 'pgadmin4.' + (new Date()).getTime().toString() + '.log');
// This function is used to read the file and return the content
const readServerLog = () => {
export const readServerLog = () => {
let data = null;
if (fs.existsSync(serverLogFile)) {
@ -168,7 +113,7 @@ const readServerLog = () => {
};
// This function is used to write the data into the log file
const writeServerLog = (data) => {
export const writeServerLog = (data) => {
data = data + '\n';
if (fs.existsSync(serverLogFile)) {
fs.writeFileSync(serverLogFile, data, {flag: 'a+'});
@ -185,28 +130,18 @@ const removeLogFile = () => {
};
// This function used to set the object of pgAdmin server process.
const setProcessObject = (processObject) => {
export const setProcessObject = (processObject) => {
pgadminServerProcess = processObject;
};
// This function used to set the object of pgAdmin window.
const setPgAdminWindowObject = (windowObject) => {
pgAdminWindowObject = windowObject;
};
// This function is used to get the server log file.
const getServerLogFile = () => {
export const getServerLogFile = () => {
return serverLogFile;
};
// This function is used to get the runtime config file.
const getRunTimeConfigFile = () => {
return configFileName;
};
// This function is used to kill the server process, remove the log files
// and quit the application.
const cleanupAndQuitApp = () => {
export const cleanupAndQuitApp = (pgAdminWindowObject) => {
// Remove the server log file on exit
removeLogFile();
@ -216,247 +151,14 @@ const cleanupAndQuitApp = () => {
process.kill(pgadminServerProcess.pid);
}
catch (e) {
console.warn('Failed to kill server process.');
console.warn('Failed to kill server process.', e);
}
}
if (pgAdminWindowObject != null) {
// Remove all the cookies.
pgAdminWindowObject.cookies.getAll({}, function(cookies) {
try {
cookies.forEach(function(cookie) {
pgAdminWindowObject.cookies.remove({url: 'http://' + cookie.domain, name: cookie.name});
});
} catch (error) {
console.warn('Failed to remove cookies.');
} finally {
pgAdminWindowObject = null;
// Quit Application
nw.App.quit();
}
const ses = pgAdminWindowObject.webContents.session;
ses.clearStorageData({
storages: ['cookies'],
});
} else {
// Quit Application
nw.App.quit();
}
};
// This function is used to create zoom events based on platform
const setZoomEvents = () => {
if (platform() == 'darwin') {
zoomInShortcut = new nw.Shortcut({key: 'Command+Equal'});
zoomOutShortcut = new nw.Shortcut({key: 'Command+Minus'});
actualSizeShortcut = new nw.Shortcut({key: 'Command+0'});
toggleFullScreenShortcut = new nw.Shortcut({key: 'Command+Ctrl+F'});
} else {
zoomInShortcut = new nw.Shortcut({key: 'Ctrl+Equal'});
zoomOutShortcut = new nw.Shortcut({key: 'Ctrl+Minus'});
actualSizeShortcut = new nw.Shortcut({key: 'Ctrl+0'});
// Use F10 instead of F11. F11 does not work possibly due to Chromium reserving it for their function.
toggleFullScreenShortcut = new nw.Shortcut({key: 'F10'});
}
zoomInShortcut.on('active', function() {
zoomIn();
});
zoomOutShortcut.on('active', function() {
zoomOut();
});
actualSizeShortcut.on('active', function() {
actualSize();
});
toggleFullScreenShortcut.on('active', function() {
toggleFullScreen();
});
zoomInShortcut.on('failed', function(msg) {
let errMsg = 'Failed to register zoom in shortcut with error: ' + msg;
console.warn(errMsg);
});
zoomOutShortcut.on('failed', function(msg) {
let errMsg = 'Failed to register zoom out shortcut with error: ' + msg;
console.warn(errMsg);
});
actualSizeShortcut.on('failed', function(msg) {
let errMsg = 'Failed to register actual size shortcut with error: ' + msg;
console.warn(errMsg);
});
toggleFullScreenShortcut.on('failed', function(msg) {
let errMsg = 'Failed to register toggle full screen shortcut with error: ' + msg;
console.warn(errMsg);
});
};
// This function is used to iterate all open windows and set the zoom level.
const setZoomLevelForAllWindows = () => {
nw.Window.getAll(function(winArray) {
for (let arr_val of winArray) {
arr_val.zoomLevel = pgAdminWindowObject.zoomLevel;
}
});
};
// This function used to zoom in the pgAdmin window.
const zoomIn = () => {
if (pgAdminWindowObject != null) {
pgAdminWindowObject.zoomLevel += 0.5;
setZoomLevelForAllWindows();
ConfigureStore.set('zoomLevel', pgAdminWindowObject.zoomLevel);
ConfigureStore.saveConfig();
}
};
// This function used to zoom out the pgAdmin window.
const zoomOut = () => {
if (pgAdminWindowObject != null) {
pgAdminWindowObject.zoomLevel -= 0.5;
setZoomLevelForAllWindows();
ConfigureStore.set('zoomLevel', pgAdminWindowObject.zoomLevel);
ConfigureStore.saveConfig();
}
};
// This function used to reset the zoom level of pgAdmin window.
const actualSize = () => {
if (pgAdminWindowObject != null) {
pgAdminWindowObject.zoomLevel = 0;
setZoomLevelForAllWindows();
ConfigureStore.set('zoomLevel', pgAdminWindowObject.zoomLevel);
ConfigureStore.saveConfig();
}
};
const toggleFullScreen = () => {
if (pgAdminWindowObject != null) {
// Toggle full screen
pgAdminWindowObject.toggleFullscreen();
// Change the menu label.
let menu_label = pgAdminWindowObject.window.document.querySelector('#mnu_toggle_fullscreen_runtime span').innerHTML;
if (menu_label.indexOf('Enter Full Screen') >= 0) {
pgAdminWindowObject.window.document.querySelector('#mnu_toggle_fullscreen_runtime span').innerHTML = menu_label.replace('Enter', 'Exit');
} else if (menu_label.indexOf('Exit Full Screen') >= 0) {
pgAdminWindowObject.window.document.querySelector('#mnu_toggle_fullscreen_runtime span').innerHTML = menu_label.replace('Exit', 'Enter');
}
}
};
// This function is used to register zoom events.
const registerZoomEvents = () => {
nw.App.registerGlobalHotKey(zoomInShortcut);
nw.App.registerGlobalHotKey(zoomOutShortcut);
nw.App.registerGlobalHotKey(actualSizeShortcut);
nw.App.registerGlobalHotKey(toggleFullScreenShortcut);
};
// This function is used to unregister zoom events.
const unregisterZoomEvents = () => {
nw.App.unregisterGlobalHotKey(zoomInShortcut);
nw.App.unregisterGlobalHotKey(zoomOutShortcut);
nw.App.unregisterGlobalHotKey(actualSizeShortcut);
nw.App.unregisterGlobalHotKey(toggleFullScreenShortcut);
};
let ConfigureStore = {
fileName: configFileName,
jsonData: {},
init: function() {
if (!this.readConfig()){
this.jsonData = DEFAULT_CONFIG_DATA;
this.saveConfig();
}
},
// This function is used to write configuration data
saveConfig: function() {
fs.writeFileSync(this.fileName, JSON.stringify(this.jsonData, null, 4), {flag: 'w'});
},
// This function is used to read the configuration data
readConfig: function() {
if (fs.existsSync(this.fileName)) {
try {
this.jsonData = JSON.parse(fs.readFileSync(this.fileName));
} catch (error) {
/* If the file is not present or invalid JSON data in file */
this.jsonData = {};
}
} else {
let errMsg = 'Unable to read file ' + this.fileName + ' not found.';
console.warn(errMsg);
return false;
}
return true;
},
getConfigData: function() {
return this.jsonData;
},
get: function(key, if_not_value) {
if(this.jsonData[key] !== undefined) {
return this.jsonData[key];
} else {
return if_not_value;
}
},
set: function(key, value) {
if(typeof key === 'object'){
this.jsonData = {
...this.jsonData,
...key,
};
} else if(value === '' || value == null || typeof(value) == 'undefined') {
if(this.jsonData[key] !== undefined) {
delete this.jsonData[key];
}
} else {
this.jsonData[key] = value;
}
},
};
function parseConsoleArgs(_method, args) {
const retData = Array.from(args).map(arg => {
try {
if(arg.stack) return arg.stack;
return JSON.stringify(arg);
} catch (e) {
return arg
}
});
return retData?.join(' ');
}
module.exports = {
readServerLog: readServerLog,
writeServerLog: writeServerLog,
getAvailablePort: getAvailablePort,
getPythonPath: getPythonPath,
setProcessObject: setProcessObject,
cleanupAndQuitApp: cleanupAndQuitApp,
getServerLogFile: getServerLogFile,
getRunTimeConfigFile: getRunTimeConfigFile,
setPgAdminWindowObject: setPgAdminWindowObject,
zoomIn: zoomIn,
zoomOut: zoomOut,
actualSize: actualSize,
toggleFullScreen: toggleFullScreen,
setZoomEvents: setZoomEvents,
registerZoomEvents: registerZoomEvents,
unregisterZoomEvents: unregisterZoomEvents,
setZoomLevelForAllWindows: setZoomLevelForAllWindows,
ConfigureStore: ConfigureStore,
parseConsoleArgs: parseConsoleArgs,
insideFlatpak: insideFlatpak,
};

View File

@ -0,0 +1,21 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
const { contextBridge, ipcRenderer } = require('electron/renderer');
contextBridge.exposeInMainWorld('electronUI', {
getConfigData: (key) => ipcRenderer.invoke('getStoreData', key),
setConfigData: (newValues) => ipcRenderer.invoke('setStoreData', newValues),
showMessageBox: (options) => ipcRenderer.invoke('showMessageBox', options),
restartApp: ()=>ipcRenderer.send('restartApp'),
getServerLogFile: ()=>ipcRenderer.invoke('getServerLogFile'),
readServerLog: ()=>ipcRenderer.invoke('readServerLog'),
checkPortAvailable: (port)=>ipcRenderer.invoke('checkPortAvailable', port),
openConfigure: ()=>ipcRenderer.invoke('openConfigure'),
});

View File

@ -6,44 +6,94 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const misc = require('../js/misc.js');
const spawn = require('child_process').spawn;
const {EOL} = require('os');
import { app, BrowserWindow, dialog, ipcMain, Menu, shell } from 'electron';
import axios from 'axios';
import Store from 'electron-store';
import fs from 'fs';
import path from 'path';
import * as misc from './misc.js';
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { setupMenu } from './menu.js';
const configStore = new Store({
defaults: {
fixedPort: false,
portNo: 5050,
connectionTimeout: 180,
openDocsInBrowser: true,
},
});
let pgadminServerProcess = null;
let startPageUrl = null;
let serverCheckUrl = null;
let addMenuCompleted = false;
let pgAdminMainScreen = null;
let serverPort = 5050;
let appStartTime = (new Date()).getTime();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let docsURLSubStrings = ['www.enterprisedb.com', 'www.postgresql.org', 'www.pgadmin.org', 'help/help'];
// Paths to the rest of the app
let pythonPath = misc.getPythonPath();
let pgadminFile = '../web/pgAdmin4.py';
let configFile = '../web/config.py';
process.env['ELECTRON_ENABLE_SECURITY_WARNINGS'] = false;
if (misc.insideFlatpak()) {
pgadminFile = '/app/pgAdmin4/web/pgAdmin4.py';
}
// Paths to the rest of the app
let [pythonPath, pgadminFile] = misc.getAppPaths(__dirname);
// Override the paths above, if a developer needs to
if (fs.existsSync('dev_config.json')) {
try {
let dev_config = JSON.parse(fs.readFileSync('dev_config.json'));
pythonPath = dev_config['pythonPath'];
pgadminFile = dev_config['pgadminFile'];
pythonPath = path.resolve(dev_config['pythonPath']);
pgadminFile = path.resolve(dev_config['pgadminFile']);
} catch (error) {
// Meh.
console.error('Failed to load dev_config', error);
}
}
Menu.setApplicationMenu(null);
function openConfigure() {
const win = new BrowserWindow({
show: false,
width: 600,
height: 600,
position: 'center',
resizable: false,
icon: '../../assets/pgAdmin4.png',
webPreferences: {
preload: path.join(__dirname, 'other_preload.js'),
},
});
win.loadFile('./src/html/configure.html');
win.once('ready-to-show', ()=>{
win.show();
});
}
function showErrorDialog(intervalID) {
if(!splashWindow.isVisible()) {
return;
}
clearInterval(intervalID);
splashWindow.close();
new BrowserWindow({
'frame': true,
'width': 790,
'height': 430,
'position': 'center',
'resizable': false,
'focus': true,
'show': true,
icon: '../../assets/pgAdmin4.png',
webPreferences: {
preload: path.join(__dirname, 'other_preload.js'),
},
}).loadFile('./src/html/server_error.html');
}
// This functions is used to start the pgAdmin4 server by spawning a
// separate process.
function startDesktopMode() {
@ -52,6 +102,7 @@ function startDesktopMode() {
if (pgadminServerProcess != null)
return;
let pingIntervalID;
let UUID = crypto.randomUUID();
// Set the environment variables so that pgAdmin 4 server
// starts listening on the appropriate port.
@ -63,28 +114,25 @@ function startDesktopMode() {
startPageUrl = 'http://127.0.0.1:' + serverPort + '/?key=' + UUID;
serverCheckUrl = 'http://127.0.0.1:' + serverPort + '/misc/ping?key=' + UUID;
document.getElementById('loader-text-status').innerHTML = 'Starting pgAdmin 4...';
// Write Python Path, pgAdmin file path and command in log file.
misc.writeServerLog('pgAdmin Runtime Environment');
misc.writeServerLog('--------------------------------------------------------');
let command = path.resolve(pythonPath) + ' -s ' + path.resolve(pgadminFile);
misc.writeServerLog('Python Path: "' + path.resolve(pythonPath) + '"');
misc.writeServerLog('Runtime Config File: "' + path.resolve(misc.getRunTimeConfigFile()) + '"');
misc.writeServerLog('pgAdmin Config File: "' + path.resolve(configFile) + '"');
misc.writeServerLog('Webapp Path: "' + path.resolve(pgadminFile) + '"');
let command = pythonPath + ' -s ' + pgadminFile;
misc.writeServerLog('Python Path: "' + pythonPath + '"');
misc.writeServerLog('Runtime Config File: "' + path.resolve(configStore.path) + '"');
misc.writeServerLog('Webapp Path: "' + pgadminFile + '"');
misc.writeServerLog('pgAdmin Command: "' + command + '"');
misc.writeServerLog('Environment: ');
Object.keys(process.env).forEach(function (key) {
// Below code is included only for Mac OS as default path for azure CLI
// installation path is not included in PATH variable while spawning
// runtime environment.
if (platform() === 'darwin' && key === 'PATH') {
if (process.platform === 'darwin' && key === 'PATH') {
let updated_path = process.env[key] + ':/usr/local/bin';
process.env[key] = updated_path;
}
if (platform() === 'win32' && key.toUpperCase() === 'PATH') {
if (process.platform === 'win32' && key.toUpperCase() === 'PATH') {
let _libpq_path = path.join(path.dirname(path.dirname(path.resolve(pgadminFile))), 'runtime');
process.env[key] = _libpq_path + ';' + process.env[key];
}
@ -95,12 +143,14 @@ function startDesktopMode() {
// Spawn the process to start pgAdmin4 server.
let spawnStartTime = (new Date).getTime();
pgadminServerProcess = spawn(path.resolve(pythonPath), ['-s', path.resolve(pgadminFile)]);
pgadminServerProcess = spawn(pythonPath, ['-s', pgadminFile]);
pgadminServerProcess.on('error', function (err) {
// Log the error into the log file if process failed to launch
misc.writeServerLog('Failed to launch pgAdmin4. Error:');
misc.writeServerLog(err);
showErrorDialog(pingIntervalID);
});
let spawnEndTime = (new Date).getTime();
misc.writeServerLog('Total spawn time to start the pgAdmin4 server: ' + (spawnEndTime - spawnStartTime) / 1000 + ' Sec');
@ -120,7 +170,7 @@ function startDesktopMode() {
return axios.get(serverCheckUrl);
}
let connectionTimeout = misc.ConfigureStore.get('connectionTimeout', 90) * 1000;
let connectionTimeout = configStore.get('connectionTimeout', 180) * 1000;
let currentTime = (new Date).getTime();
let endTime = currentTime + connectionTimeout;
let midTime1 = currentTime + (connectionTimeout / 2);
@ -129,7 +179,7 @@ function startDesktopMode() {
// ping pgAdmin server every 1 second.
let pingStartTime = (new Date).getTime();
let intervalID = setInterval(function () {
pingIntervalID = setInterval(function () {
// If ping request is already send and response is not
// received no need to send another request.
if (pingInProgress)
@ -137,11 +187,11 @@ function startDesktopMode() {
pingServer().then(() => {
pingInProgress = false;
document.getElementById('loader-text-status').innerHTML = 'pgAdmin 4 started';
splashWindow.webContents.executeJavaScript('document.getElementById(\'loader-text-status\').innerHTML = \'pgAdmin 4 started\';', true);
// Set the pgAdmin process object to misc
misc.setProcessObject(pgadminServerProcess);
clearInterval(intervalID);
clearInterval(pingIntervalID);
let appEndTime = (new Date).getTime();
misc.writeServerLog('------------------------------------------');
misc.writeServerLog('Total time taken to ping pgAdmin4 server: ' + (appEndTime - pingStartTime) / 1000 + ' Sec');
@ -155,33 +205,20 @@ function startDesktopMode() {
// if the connection timeout has lapsed then throw an error
// and stop pinging the server.
if (curTime >= endTime) {
clearInterval(intervalID);
splashWindow.hide();
nw.Window.open('src/html/server_error.html', {
'frame': true,
'width': 790,
'height': 430,
'position': 'center',
'resizable': false,
'focus': true,
'show': true,
});
showErrorDialog(pingIntervalID);
}
if (curTime > midTime1) {
if (curTime < midTime2) {
document.getElementById('loader-text-status').innerHTML = 'Taking longer than usual...';
splashWindow.webContents.executeJavaScript('document.getElementById(\'loader-text-status\').innerHTML = \'Taking longer than usual...\';', true);
} else {
document.getElementById('loader-text-status').innerHTML = 'Almost there...';
splashWindow.webContents.executeJavaScript('document.getElementById(\'loader-text-status\').innerHTML = \'Almost there...\';', true);
}
} else {
document.getElementById('loader-text-status').innerHTML = 'Waiting for pgAdmin 4 to start...';
}
});
pingInProgress = true;
}, 250);
}, 1000);
}
// This function is used to hide the splash screen and create/launch
@ -189,7 +226,7 @@ function startDesktopMode() {
function launchPgAdminWindow() {
// Create and launch new window and open pgAdmin url
misc.writeServerLog('Application Server URL: ' + startPageUrl);
nw.Window.open(startPageUrl, {
pgAdminMainScreen = new BrowserWindow({
'id': 'pgadmin-main',
'icon': '../../assets/pgAdmin4.png',
'frame': true,
@ -201,503 +238,150 @@ function launchPgAdminWindow() {
'height': 768,
'focus': true,
'show': false,
}, (pgadminWindow) => {
pgAdminMainScreen = pgadminWindow;
// Set pgAdmin4 Windows Object
misc.setPgAdminWindowObject(pgadminWindow);
// Set the zoom level stored in the config file.
pgadminWindow.zoomLevel = misc.ConfigureStore.get('zoomLevel', 0);
// Set zoom in and out events.
misc.setZoomEvents();
// Workaround to fix increasing window size.
// https://github.com/nwjs/nw.js/issues/7973
pgadminWindow.on('close', function () {
// Resize Window
let resizeHeightBy = pgadminWindow.window.outerHeight - pgadminWindow.window.innerHeight;
pgadminWindow.resizeBy(0, -resizeHeightBy);
// Remove 'close' event handler, and then close window
pgadminWindow.removeAllListeners('close');
pgadminWindow.close()
});
pgadminWindow.on('closed', function () {
misc.cleanupAndQuitApp();
});
// set up handler for new-win-policy event.
// Set the width and height for the new window.
pgadminWindow.on('new-win-policy', function (frame, url, policy) {
if (!frame) {
let openDocsInBrowser = misc.ConfigureStore.get('openDocsInBrowser', true);
let isDocURL = false;
docsURLSubStrings.forEach(function (key) {
if (url.indexOf(key) >= 0) {
isDocURL = true;
}
});
if (openDocsInBrowser && isDocURL) {
// Do not open the window
policy.ignore();
// Open URL in the external browser.
nw.Shell.openExternal(url);
} else {
policy.setNewWindowManifest({
'icon': '../../assets/pgAdmin4.png',
'frame': true,
'position': 'center',
'min_width': 640,
'min_height': 480,
'width': pgadminWindow.width,
'height': pgadminWindow.height,
});
}
}
});
pgadminWindow.on('loaded', function () {
/* Make the new window opener to null as it is
* nothing but a splash screen. We will have to make it null,
* so that open in new browser tab will work.
*/
pgadminWindow.window.hookConsole((method, args)=>{
misc.writeServerLog(
`--------------[UI ${method}]---------------${EOL}${misc.parseConsoleArgs(method, args)}${EOL}------------[UI End]----------------`);
});
pgadminWindow.window.opener = null;
// Show new window
pgadminWindow.show();
pgadminWindow.focus();
nativeMenu = new gui.Menu({ type: 'menubar' });
// Create Mac Builtin Menu
if (platform() === 'darwin') {
nativeMenu.createMacBuiltin('pgAdmin 4');
// Remove 'About pgAdmin 4' submenu
nativeMenu?.items[0].submenu.removeAt(0);
// Remove 'Close Window' submenu
nativeMenu?.items[2].submenu.removeAt(1);
pgAdminMainScreen.menu = nativeMenu;
}
try {
pgAdminMainScreen.isCustomMenusAdded = false;
let addMenuInterval = setInterval(() => {
if (pgadminWindow?.window?.pgAdmin?.Browser?.Events && pgadminWindow?.window?.pgAdmin?.Browser?.MainMenus?.length > 0) {
pgadminWindow.window.pgAdmin.Browser.Events.on('pgadmin:nw-enable-disable-menu-items', enableDisableMenuItem);
pgadminWindow.window.pgAdmin.Browser.Events.on('pgadmin:nw-refresh-menu-item', refreshMenuItems);
pgadminWindow.window.pgAdmin.Browser.Events.on('pgadmin:nw-update-checked-menu-item', updateCheckedMenuItem);
pgadminWindow.window.pgAdmin.Browser.Events.on('pgadmin:nw-set-new-window-open-size', setNewWindowSize)
// Add Main Menus to native menu.
pgadminWindow.window.pgAdmin.Browser.MainMenus.forEach((menu)=> {
addMenu(menu)
})
clearInterval(addMenuInterval);
}
}, 250)
} catch (e) {
console.error('Error in add native menus');
}
// Hide the splash screen
splashWindow.hide();
});
pgadminWindow.on('blur', function () {
misc.unregisterZoomEvents();
});
pgadminWindow.on('focus', function () {
misc.registerZoomEvents();
});
});
}
// Get the gui object of NW.js
let gui = require('nw.gui');
let splashWindow = gui.Window.get();
// Enable dragging on the splash screen.
let isDragging = false;
let dragOrigin = { x: 0, y: 0 };
document.mouseleave = () => isDragging = false;
document.onmouseup = () => isDragging = false;
document.onmousedown = (e) => {
isDragging = true;
dragOrigin.x = e.x;
dragOrigin.y = e.y;
};
document.onmousemove = (e) => {
if (isDragging) {
splashWindow.moveTo(e.screenX - dragOrigin.x, e.screenY - dragOrigin.y);
}
};
// Always clear the cache before starting the application.
nw.App.clearCache();
let nativeMenu;
splashWindow.on('loaded', function () {
// Initialize the ConfigureStore
misc.ConfigureStore.init();
let fixedPortCheck = misc.ConfigureStore.get('fixedPort', false);
if (fixedPortCheck) {
serverPort = misc.ConfigureStore.get('portNo');
//Start the pgAdmin in Desktop mode.
startDesktopMode();
} else {
// get the available TCP port by sending port no to 0.
misc.getAvailablePort(0)
.then((pythonApplicationPort) => {
serverPort = pythonApplicationPort;
//Start the pgAdmin in Desktop mode.
startDesktopMode();
})
.catch((errCode) => {
if (errCode === 'EADDRINUSE') {
alert('The port specified is already in use. Please enter a free port number.');
} else {
alert(errCode);
}
});
}
});
splashWindow.on('close', function () {
misc.cleanupAndQuitApp();
});
function setNewWindowSize(){
misc.setZoomLevelForAllWindows();
}
function addCommonMenus(menu) {
let _menu = new gui.Menu();
menu.menuItems.forEach((menuItem) => {
let submenu = getSubMenu(menuItem);
let _menuItem = new gui.MenuItem({
label: menuItem.label,
enabled: !menuItem.isDisabled,
type: menuItem.type || 'normal',
priority: menuItem.priority,
...(submenu.items.length > 0) && {
submenu: submenu,
},
click: function () {
menuItem.callback();
},
});
_menu.append(_menuItem);
webPreferences: {
nodeIntegrationInSubFrames: true,
preload: path.join(__dirname, 'pgadmin_preload.js'),
},
});
if (menu.name == 'file') {
let runtimeMenu = getRuntimeMenu();
_menu.append(runtimeMenu);
}
splashWindow.close();
pgAdminMainScreen.webContents.session.clearCache();
if (menu.menuItems.length == 0) {
let _menuItem = new gui.MenuItem({
label: 'No object selected',
enabled: false,
priority: 0,
});
_menu.append(_menuItem);
}
if (platform() == 'darwin') {
pgAdminMainScreen.menu.insert(new gui.MenuItem({
label: menu.label,
name: menu.name,
submenu: _menu,
}), menu.index);
} else {
nativeMenu.append(new gui.MenuItem({
label: menu.label,
name: menu.name,
submenu: _menu,
}));
pgAdminMainScreen.menu = nativeMenu;
}
}
function getRuntimeMenu() {
let subMenus = new gui.Menu();
let rtmenudt = pgAdminMainScreen.window.pgAdmin.Browser.RUNTIME_MENUS_OPTIONS['runtime']
let runtimeSubMenus = pgAdminMainScreen.window.pgAdmin.Browser.RUNTIME_MENUS_OPTIONS['runtime']['submenus']
subMenus.append(new gui.MenuItem({
label: runtimeSubMenus['configure'].label,
enabled: runtimeSubMenus['configure'].enable,
priority: runtimeSubMenus['configure'].priority,
type: 'normal',
checked: false,
click: function () {
// Create and launch new window and open pgAdmin url
nw.Window.open('src/html/configure.html', {
'frame': true,
'width': 600,
'height': 585,
'position': 'center',
'resizable': false,
'focus': true,
'show': true,
});
},
}));
subMenus.append(new gui.MenuItem({
label: runtimeSubMenus['view_log'].label,
enabled: runtimeSubMenus['view_log'].enable,
priority: runtimeSubMenus['view_log'].priority,
type: 'normal',
checked: false,
click: function () {
// Create and launch new window and open pgAdmin url
nw.Window.open('src/html/view_log.html', {
'frame': true,
'width': 790,
'height': 425,
'position': 'center',
'resizable': false,
'focus': true,
'show': true,
});
},
}));
subMenus.append(new nw.MenuItem({ type: 'separator' }));
subMenus.append(new gui.MenuItem({
label: pgAdminMainScreen?.isFullscreen ? runtimeSubMenus['exit_full_screen'].label : runtimeSubMenus['enter_full_screen'].label,
enabled: runtimeSubMenus['enter_full_screen'].enable,
priority: runtimeSubMenus['enter_full_screen'].priority,
type: 'normal',
checked: false,
key: runtimeSubMenus['enter_full_screen'].key,
modifiers: runtimeSubMenus['enter_full_screen'].modifiers,
click: function () {
this.label = !pgAdminMainScreen?.isFullscreen ? runtimeSubMenus['exit_full_screen'].label : runtimeSubMenus['enter_full_screen'].label;
misc.toggleFullScreen();
},
}));
subMenus.append(new gui.MenuItem({
label: runtimeSubMenus['actual_size'].label,
enabled: runtimeSubMenus['actual_size'].enable,
priority: runtimeSubMenus['actual_size'].priority,
type: 'normal',
checked: false,
key: runtimeSubMenus['actual_size'].key,
modifiers: runtimeSubMenus['actual_size'].modifiers,
click: function () {
misc.actualSize();
},
}));
subMenus.append(new gui.MenuItem({
label: runtimeSubMenus['zoom_in'].label,
enabled: runtimeSubMenus['zoom_in'].enable,
priority: runtimeSubMenus['zoom_in'].priority,
type: 'normal',
checked: false,
key: runtimeSubMenus['zoom_in'].key,
modifiers: runtimeSubMenus['zoom_in'].modifiers,
click: function () {
misc.zoomIn();
},
}));
subMenus.append(new gui.MenuItem({
label: runtimeSubMenus['zoom_out'].label,
enabled: runtimeSubMenus['zoom_out'].enable,
priority: runtimeSubMenus['zoom_out'].priority,
type: 'normal',
checked: false,
key: runtimeSubMenus['zoom_out'].key,
modifiers: runtimeSubMenus['zoom_out'].modifiers,
click: function () {
misc.zoomOut();
},
}));
let runtimeMenu = new gui.MenuItem({
label: rtmenudt.label,
enabled: true,
priority: rtmenudt.priority,
type: 'normal',
checked: false,
submenu: subMenus,
})
return runtimeMenu;
}
function getSubMenu(menuItem) {
let submenu = new gui.Menu();
if (menuItem.menu_items) {
menuItem.menu_items.forEach((item) => {
let menuType = typeof item.checked == 'boolean' ? 'checkbox' : item.type;
submenu.append(new gui.MenuItem({
label: item.label,
enabled: !item.isDisabled,
priority: item.priority,
type: menuType,
checked: item.checked,
click: function () {
if (menuType == 'checkbox') {
pgAdminMainScreen.menu.items.forEach(el => {
el.submenu.items.forEach((sub) => {
if (sub.submenu?.items?.length) {
sub.submenu.items.forEach((m) => {
if (m.type == 'checkbox') {
m.checked = m.label == item.label;
}
});
}
});
});
}
item.callback();
setupMenu(pgAdminMainScreen, {
'view_logs': ()=>{
const win = new BrowserWindow({
show: false,
width: 790,
height: 425,
position: 'center',
resizable: false,
icon: '../../assets/pgAdmin4.png',
webPreferences: {
preload: path.join(__dirname, 'other_preload.js'),
},
}));
});
}
return submenu;
}
function addMacMenu(menu) {
if (menu.name == 'file' && platform() === 'darwin') {
let rootMenu = nativeMenu.items[0].submenu;
let indx = 0;
menu.menuItems.forEach((menuItem) => {
let submenu = getSubMenu(menuItem);
rootMenu.insert(
new gui.MenuItem({
label: menuItem.label,
type: menuItem.type || 'normal',
enabled: !menuItem.isDisabled,
priority: menuItem.priority,
...(submenu.items.length > 0) && {
submenu: submenu,
},
click: function () {
// Callback functions for actions
menuItem.callback();
},
}), indx);
indx++;
});
let runtimeMenu = getRuntimeMenu();
rootMenu.insert(runtimeMenu, indx++);
let separator_menu = new nw.MenuItem({ type: 'separator' });
rootMenu.insert(separator_menu, indx);
indx++;
pgAdminMainScreen.menu = nativeMenu;
} else {
addCommonMenus(menu)
}
}
function addOtherOsMenu(menu) {
addCommonMenus(menu)
}
function addMenu(menu) {
pgAdminMainScreen.isCustomMenusAdded = true;
if (platform() === 'darwin') {
addMacMenu(menu);
} else {
addOtherOsMenu(menu);
}
addMenuCompleted = true;
}
function enableDisableMenuItem(menu, menuItem) {
if (addMenuCompleted) {
// Enable or Disabled specific menu item
pgAdminMainScreen.menu.items.forEach(el => {
if (el?.label == menu?.label) {
el.submenu.items.forEach((sub) => {
if (sub.label == menuItem.label) {
sub.enabled = !menuItem.isDisabled;
}
});
}
});
}
}
function updateCheckedMenuItem(menuItem) {
// check/ uncheck specific menu item
pgAdminMainScreen.menu.items.forEach(el => {
el.submenu.items.forEach((sub) => {
if(sub.label == menuItem.parentMenu.label) {
sub.submenu.items.forEach((sm)=> {
if (sm.label == menuItem.label && sm.type == 'checkbox') {
sm.checked = menuItem.checked
}
})
} else if (sub.label == menuItem.label && type == 'checkbox') {
sub.checked = menuItem.checked
}
});
});
}
function refreshMenuItems(menu) {
// Add menu item/option in specific menu.
pgAdminMainScreen.menu.items.forEach(el => {
if (el.label == menu.label) {
let totalSubItems = el.submenu.items.length;
// Remove exisitng menu options to add new options.
for (let i = 0; i < totalSubItems; i++) {
el.submenu.removeAt(0);
}
menu.menuItems.forEach((item) => {
let submenu = new gui.Menu();
if (item.menu_items) {
item.menu_items.forEach((subItem) => {
submenu.append(new gui.MenuItem({
label: subItem.label,
enabled: !subItem.isDisabled,
priority: subItem.priority,
type: [true, false].includes(subItem.checked) ? 'checkbox' : 'normal',
checked: subItem.checked,
click: function () {
subItem.callback();
},
}));
});
}
let _menuItem = new gui.MenuItem({
label: item.label,
enabled: !item.isDisabled,
priority: item.priority,
type: item.type,
...(submenu.items.length > 0) && {
submenu: submenu,
},
click: function () {
item.callback();
},
});
el.submenu.append(_menuItem);
});
win.loadFile('./src/html/view_log.html');
win.once('ready-to-show', ()=>{
win.show();
});
},
'configure': openConfigure,
});
pgAdminMainScreen.loadURL(startPageUrl);
pgAdminMainScreen.setBounds(configStore.get('bounds'));
pgAdminMainScreen.show();
pgAdminMainScreen.webContents.setWindowOpenHandler(({url})=>{
let openDocsInBrowser = configStore.get('openDocsInBrowser', true);
let isDocURL = false;
docsURLSubStrings.forEach(function (key) {
if (url.indexOf(key) >= 0) {
isDocURL = true;
}
});
if (openDocsInBrowser && isDocURL) {
// Do not open the window
shell.openExternal(url);
return { action: 'deny' };
} else {
return {
action: 'allow',
overrideBrowserWindowOptions: {
'position': 'center',
'min_width': 640,
'min_height': 480,
icon: '../../assets/pgAdmin4.png',
...pgAdminMainScreen.getBounds(),
webPreferences: {
preload: path.join(__dirname, 'pgadmin_preload.js'),
},
},
};
}
});
pgAdminMainScreen.on('close', () => {
configStore.set('bounds', pgAdminMainScreen.getBounds());
misc.cleanupAndQuitApp(pgAdminMainScreen);
pgAdminMainScreen.removeAllListeners('close');
pgAdminMainScreen.close();
});
}
let splashWindow;
// setup preload events.
ipcMain.handle('showOpenDialog', (_e, options) => dialog.showOpenDialog(options));
ipcMain.handle('showSaveDialog', (_e, options) => dialog.showSaveDialog(options));
ipcMain.handle('showMessageBox', (_e, options) => dialog.showMessageBox(options));
ipcMain.handle('getStoreData', (_e, key) => key ? configStore.get(key) : configStore.store);
ipcMain.handle('setStoreData', (_e, newValues) => {
configStore.store = {
...configStore.store,
...newValues,
};
});
ipcMain.handle('getServerLogFile', () => misc.getServerLogFile());
ipcMain.handle('readServerLog', () => misc.readServerLog());
ipcMain.handle('restartApp', ()=>{
app.relaunch();
app.exit(0);
});
ipcMain.handle('log', (text) => ()=>{
misc.writeServerLog(text);
});
ipcMain.handle('checkPortAvailable', async (_e, fixedPort)=>{
try {
await misc.getAvailablePort(fixedPort);
return true;
} catch {
return false;
}
});
ipcMain.handle('openConfigure', openConfigure);
app.whenReady().then(() => {
splashWindow = new BrowserWindow({
transparent: true,
width: 750,
height: 600,
frame: false,
movable: true,
focusable: true,
resizable: false,
show: false,
icon: '../../assets/pgAdmin4.png',
});
splashWindow.loadFile('./src/html/splash.html');
splashWindow.center();
splashWindow.on('show', function () {
let fixedPortCheck = configStore.get('fixedPort', false);
if (fixedPortCheck) {
serverPort = configStore.get('portNo');
//Start the pgAdmin in Desktop mode.
startDesktopMode();
} else {
// get the available TCP port by sending port no to 0.
misc.getAvailablePort(0)
.then((pythonApplicationPort) => {
serverPort = pythonApplicationPort;
//Start the pgAdmin in Desktop mode.
startDesktopMode();
})
.catch((errCode) => {
if (errCode === 'EADDRINUSE') {
dialog.showErrorBox('Error', 'The port specified is already in use. Please enter a free port number.');
} else {
dialog.showErrorBox('Error', errCode.toString());
}
splashWindow.close();
});
}
});
splashWindow.show();
});

View File

@ -0,0 +1,26 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
const { contextBridge, ipcRenderer } = require('electron/renderer');
contextBridge.exposeInMainWorld('electronUI', {
onMenuClick: (callback) => ipcRenderer.on('menu-click', (_event, details) => callback(details)),
setMenus: (menus) => {
ipcRenderer.send('setMenus', menus);
},
enableDisableMenuItems: (menu, item) => {
ipcRenderer.send('enable-disable-menu-items', menu, item);
},
setMenuItems: (menu, menuItems) => {
ipcRenderer.send('set-menu-items', menu, menuItems);
},
showOpenDialog: (options) => ipcRenderer.invoke('showOpenDialog', options),
showSaveDialog: (options) => ipcRenderer.invoke('showSaveDialog', options),
log: (text)=> ipcRenderer.send('log', text),
});

View File

@ -1,34 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
const misc = require('../js/misc.js');
// Get the window object of server error window
let gui = require('nw.gui');
let errorWindow = gui.Window.get();
errorWindow.on('loaded', function() {
document.getElementById('server_error_label').innerHTML = 'The pgAdmin 4 server could not be contacted:';
document.getElementById('server_error_log').innerHTML = misc.readServerLog();
document.getElementById('btnConfigure').addEventListener('click', function() {
nw.Window.open('src/html/configure.html', {
'frame': true,
'width': 600,
'height': 420,
'position': 'center',
'resizable': false,
'focus': true,
'show': true,
});
});
});
errorWindow.on('close', function() {
misc.cleanupAndQuitApp();
});

View File

@ -1,27 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
const misc = require('../js/misc.js');
// Get the window object of view log window
let gui = require('nw.gui');
let logWindow = gui.Window.get();
logWindow.on('loaded', function() {
document.getElementById('status-text').innerHTML = '';
document.getElementById('server_log_label').innerHTML = 'Server Log: ' + '(' + misc.getServerLogFile() + ')';
document.getElementById('server_log').innerHTML = misc.readServerLog();
document.getElementById('btnReload').addEventListener('click', function() {
document.getElementById('server_log').innerHTML = 'Loading logs...';
setTimeout(function() {
document.getElementById('server_log').innerHTML = misc.readServerLog();
}, 500);
document.getElementById('status-text').innerHTML = 'Logs reloaded successfully';
});
});

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,10 @@ from pgadmin.utils.menu import MenuItem
from pgadmin.utils.constants import MIMETYPE_APP_JS
from pgadmin.utils.ajax import make_json_response
import config
import httpagentparser
from pgadmin.model import User
from user_agents import parse
import platform
import re
MODULE_NAME = 'about'
@ -59,11 +59,11 @@ def index():
"""Render the about box."""
info = {}
# Get OS , NW.js, Browser details
browser, os_details, nwjs_version = detect_browser(request)
browser, os_details, electron_version = detect_browser(request)
admin = is_admin(current_user.email)
if nwjs_version:
info['nwjs'] = nwjs_version
if electron_version:
info['electron'] = electron_version
if config.SERVER_MODE:
info['app_mode'] = gettext('Server')
@ -119,32 +119,19 @@ def is_admin(load_user):
def detect_browser(request):
"""This function returns the browser and os details"""
nwjs_version = None
electron_version = None
agent = request.environ.get('HTTP_USER_AGENT')
os_details = parse(platform.platform()).ua_string
if 'Nwjs' in agent:
agent = agent.split('-')
nwjs_version = agent[0].split(':')[1]
browser = 'Chromium' + ' ' + agent[2]
if 'Electron' in agent:
electron_version = re.findall('Electron/([\\d.]+\\d+)', agent)[0]
browser = re.findall(
'(opera|chrome|safari|firefox|msie|trident(?=/))/?\\s*([\\d.]+\\d+)',
agent, re.IGNORECASE)
if not browser:
browser = agent.split('/')[0]
else:
browser = httpagentparser.detect(agent)
if not browser:
browser = agent.split('/')[0]
else:
browser = browser['browser']['name'] + ' ' + browser['browser'][
'version']
browser = " ".join(browser[0])
return browser, os_details, nwjs_version
@blueprint.route("/about.js")
@pga_login_required
def script():
"""render the required javascript"""
return Response(
response=render_template("about/about.js", _=gettext),
status=200,
mimetype=MIMETYPE_APP_JS
)
return browser, os_details, electron_version

View File

@ -65,13 +65,13 @@ export default function AboutComponent() {
<InputLabel>{aboutData.current_user}</InputLabel>
</Grid>
</Grid>
{ aboutData.nwjs &&
{ aboutData.electron &&
<Grid container spacing={0} style={{marginBottom: '8px'}}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('NW.js Version')}</InputLabel>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('Electron Version')}</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<InputLabel>{aboutData.nwjs}</InputLabel>
<InputLabel>{aboutData.electron}</InputLabel>
</Grid>
</Grid>
}

View File

@ -22,7 +22,7 @@ const MAIN_MENUS = [
];
let { name: browser } = getBrowser();
if (browser == 'Nwjs') {
if (browser == 'Electron') {
let controlKey = isMac() ? 'cmd' : 'ctrl';
let fullScreenKey = isMac() ? 'F' : 'F10';
@ -47,9 +47,31 @@ if (browser == 'Nwjs') {
}
export default class MainMenuFactory {
static electronCallbacks = {};
static toElectron() {
// we support 2 levels of submenu
return pgAdmin.Browser.MainMenus.map((m)=>{
return {
...m.serialize(),
submenu: m.menuItems.map((sm)=>{
const smName = `${m.name}_${sm.name}`;
MainMenuFactory.electronCallbacks[smName] = sm.callback;
return {
...sm.serialize(),
submenu: sm.getMenuItems()?.map((smsm)=>{
MainMenuFactory.electronCallbacks[`${smName}_${smsm.name}`] = sm.callback;
return {
...smsm.serialize(),
};
})
};
})
};
});
}
static createMainMenus() {
pgAdmin.Browser.MainMenus = [];
MAIN_MENUS.forEach((_menu) => {
@ -70,6 +92,12 @@ export default class MainMenuFactory {
});
pgAdmin.Browser.enable_disable_menus();
window.electronUI?.onMenuClick((menuName)=>{
MainMenuFactory.electronCallbacks[menuName]?.();
});
window.electronUI?.setMenus(MainMenuFactory.toElectron());
}
static getSeparator(label, priority) {
@ -78,7 +106,8 @@ export default class MainMenuFactory {
static refreshMainMenuItems(menu, menuItems) {
menu.setMenuItems(menuItems);
pgAdmin.Browser.Events.trigger('pgadmin:nw-refresh-menu-item', menu);
window.electronUI?.setMenus(MainMenuFactory.toElectron());
pgAdmin.Browser.Events.trigger('pgadmin:nw-refresh-menu-item', pgAdmin.Browser.MainMenus);
}
static createMenuItem(options) {
@ -102,6 +131,7 @@ export default class MainMenuFactory {
}
}}, (menu, item)=> {
pgAdmin.Browser.Events.trigger('pgadmin:nw-enable-disable-menu-items', menu, item);
window.electronUI?.enableDisableMenuItems(menu?.serialize(), item?.serialize());
}, (item) => {
pgAdmin.Browser.Events.trigger('pgadmin:nw-update-checked-menu-item', item);
});

View File

@ -3,15 +3,27 @@
{% block title %}{{ config.APP_NAME }}{% endblock %}
{% block init_script %}
window.hookConsole = function(callback) {
for (const method of ['log', 'error']) {
const nativeMethod = window.console[method];
window.console[method] = function () {
nativeMethod.apply(this, arguments);
setTimeout(()=>{
callback(method, arguments);
});
function parseConsoleArgs(args) {
const retData = Array.from(args).map(arg => {
try {
if(arg.stack) return arg.stack;
return JSON.stringify(arg);
} catch (e) {
return arg
}
});
return retData?.join(' ');
}
for (const method of ['log', 'error']) {
const nativeMethod = window.console[method];
window.console[method] = function () {
nativeMethod.apply(this, arguments);
setTimeout(()=>{
window.electronUI?.log(`--------------[UI ${method}]---------------
${parseConsoleArgs(arguments)}
------------[UI End]----------------`);
});
}
}
try {

View File

@ -85,30 +85,35 @@ export default class FileManagerModule {
});
}
showNative(params, onOK, onCancel) {
// https://docs.nwjs.io/en/latest/References/Changes%20to%20DOM/
let fileEle = document.createElement('input');
let accept = params.supported_types?.map((v)=>(v=='*' ? '' : `.${v}`))?.join(',');
fileEle.setAttribute('type', 'file');
fileEle.setAttribute('accept', accept);
fileEle.onchange = (e)=>{
if(e.target.value) {
onOK?.(e.target.value);
} else {
onCancel?.();
}
};
async showNative(params, onOK, onCancel) {
let res;
let options = {};
options['filters'] = params.supported_types?.map((v)=>(
v=='*' ? {name: 'All Files', extensions: ['*']} :
{name: `${v.toUpperCase()} File .${v}`, extensions:[v]}
));
if(params.dialog_type == 'create_file') {
fileEle.setAttribute('nwsaveas', '');
} else if(params.dialog_type == 'select_folder') {
fileEle.setAttribute('nwdirectory', '');
res = await window.electronUI.showSaveDialog(options);
} else {
options['properties'] = ['openFile'];
if(params.dialog_type == 'select_folder') {
options['properties'] = ['openDirectory'];
}
res = await window.electronUI.showOpenDialog(options);
}
if(res.canceled) {
onCancel?.();
} else {
onOK?.(res.filePaths[0]);
}
fileEle.dispatchEvent(new MouseEvent('click'));
}
show(params, onOK, onCancel, modalObj) {
let {name: browser} = getBrowser();
if(browser == 'Nwjs') {
if(browser == 'Electron') {
try {
this.showNative(params, onOK, onCancel);
} catch {

View File

@ -111,8 +111,8 @@ export default function BrowserComponent({pgAdmin}) {
<PgAdminContext.Provider value={pgAdmin}>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} onReady={()=>setUiReady(true)}/>
{browser != 'Nwjs' && <AppMenuBar />}
<div style={{height: (browser != 'Nwjs' ? 'calc(100% - 30px)' : '100%')}}>
{browser != 'Electron' && <AppMenuBar />}
<div style={{height: (browser != 'Electron' ? 'calc(100% - 30px)' : '100%')}}>
<Layout
getLayoutInstance={(obj)=>{
pgAdmin.Browser.docker = obj;

View File

@ -24,6 +24,16 @@ export default class Menu {
return menuObj;
}
serialize() {
return {
id: this.id,
label: this.label,
name: this.name,
index: this.index,
addSepratior: this.addSepratior,
};
}
addMenuItem(menuItem, index=null) {
if (menuItem instanceof MenuItem) {
menuItem.parentMenu = this;
@ -116,7 +126,6 @@ export class MenuItem {
url: '#',
target: '_self',
enable: true,
type: 'normal'
};
_.extend(this, defaults, _.pick(options, menu_opts));
if (!this.callback) {
@ -136,6 +145,17 @@ export class MenuItem {
return MenuItem(options);
}
serialize() {
return {
name: this.name,
label: this.label,
enabled: !this.isDisabled,
priority: this.priority,
type: [true, false].includes(this.checked) ? 'checkbox' : this.type,
checked: this.checked,
};
}
change_checked(isChecked) {
this.checked = isChecked;
this.changeChecked?.(this);

View File

@ -377,14 +377,17 @@ export function evalFunc(obj, func, ...param) {
}
export function getBrowser() {
if(navigator.userAgent.indexOf('Electron') >= 0) {
return {name: 'Electron', version: navigator.userAgent.match(/Electron\/([\d\.]+\d+)/)[1]};
}
let ua=navigator.userAgent,tem,M=(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i).exec(ua) || [];
if(/trident/i.test(M[1])) {
tem=/\brv[ :]+(\d+)/g.exec(ua) || [];
return {name:'IE', version:(tem[1]||'')};
}
if(ua.startsWith('Nwjs')) {
let nwjs = ua.split('-')[0]?.split(':');
return {name:nwjs[0], version: nwjs[1]};
if(ua.indexOf('Electron') >= 0) {
return {name: 'Electron', version: ua.match(/Electron\/([\d\.]+\d+)/)[1]};
}
if(M[1]==='Chrome') {

View File

@ -24,7 +24,7 @@ import { Box } from '@mui/material';
import { getDatabaseLabel, getTitle, setQueryToolDockerTitle } from '../sqleditor_title';
import gettext from 'sources/gettext';
import NewConnectionDialog from './dialogs/NewConnectionDialog';
import { evalFunc } from '../../../../../static/js/utils';
import { evalFunc, getBrowser } from '../../../../../static/js/utils';
import { Notifications } from './sections/Notifications';
import MacrosDialog from './dialogs/MacrosDialog';
import FilterDialog from './dialogs/FilterDialog';
@ -405,6 +405,12 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
});
};
const onBeforeUnloadElectron = (e)=>{
e.preventDefault();
e.returnValue = 'prevent';
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.WARN_SAVE_DATA_CLOSE);
};
useEffect(()=>{
getSQLScript();
initializeQueryTool();
@ -418,7 +424,14 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
});
eventBus.current.registerListener(QUERY_TOOL_EVENTS.FORCE_CLOSE_PANEL, ()=>{
qtPanelDocker.close(qtPanelId, true);
if(getBrowser().name == 'Electron' && qtState.is_new_tab) {
window.removeEventListener('beforeunload', onBeforeUnloadElectron);
// somehow window.close was not working may becuase the removeEventListener
// was not completely executed. Add timeout.
setTimeout(()=>window.close(), 50);
} else {
qtPanelDocker.close(qtPanelId, true);
}
});
qtPanelDocker.eventBus.registerListener(LAYOUT_EVENTS.CLOSING, (id)=>{
@ -561,7 +574,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
const events = [
[QUERY_TOOL_EVENTS.TRIGGER_LOAD_FILE, ()=>{
let fileParams = {
'supported_types': ['*', 'sql'], // file types allowed
'supported_types': ['sql', '*'], // file types allowed
'dialog_type': 'select_file', // open select file dialog
};
pgAdmin.Tools.FileManager.show(fileParams, (fileName, storage)=>{
@ -573,7 +586,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE, qtState.current_file);
} else {
let fileParams = {
'supported_types': ['*', 'sql'],
'supported_types': ['sql', '*'],
'dialog_type': 'create_file',
'dialog_title': 'Save File',
'btn_primary': 'Save',
@ -632,13 +645,18 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
useEffect(()=> {
// Add beforeunload event if "Confirm on close or refresh" option is enabled in the preferences.
if(qtState.preferences.browser.confirm_on_refresh_close){
window.addEventListener('beforeunload', onBeforeUnload);
if(getBrowser().name == 'Electron') {
window.addEventListener('beforeunload', onBeforeUnloadElectron);
} else {
window.removeEventListener('beforeunload', onBeforeUnload);
if(qtState.preferences.browser.confirm_on_refresh_close){
window.addEventListener('beforeunload', onBeforeUnload);
} else {
window.removeEventListener('beforeunload', onBeforeUnload);
}
}
return () => {
window.removeEventListener('beforeunload', onBeforeUnloadElectron);
window.removeEventListener('beforeunload', onBeforeUnload);
};
}, [qtState.preferences.browser]);