Enhanced pgAdmin 4 with support for Workspace layouts. #7708

This commit is contained in:
Akshay Joshi 2024-12-16 14:52:56 +05:30 committed by GitHub
parent 4e2fd404c0
commit fe6e21a08b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
125 changed files with 4052 additions and 611 deletions

View File

@ -53,11 +53,11 @@ The default binary paths set in the container are as follows:
.. code-block:: bash
DEFAULT_BINARY_PATHS = {
'pg-17': '/usr/local/pgsql-17',
'pg-16': '/usr/local/pgsql-16',
'pg-15': '/usr/local/pgsql-15',
'pg-14': '/usr/local/pgsql-14',
'pg-13': '/usr/local/pgsql-13',
'pg-12': '/usr/local/pgsql-12'
'pg-13': '/usr/local/pgsql-13'
}
this may be changed in the :ref:`preferences`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -1,6 +1,6 @@
****************************
`Keyboard Shortcuts`:index::
****************************
***************************
`Keyboard Shortcuts`:index:
***************************
Keyboard shortcuts are provided in pgAdmin to allow easy access to specific
functions. Alternate shortcuts can be configured through File > Preferences if

View File

@ -6,7 +6,14 @@
***************************
Use options on the *Preferences* dialog to customize the behavior of the client.
To open the *Preferences* dialog, select *Preferences* from the *File* menu.
To open the *Preferences* dialog, select *Preferences* from the *File* menu or
click on the *Settings* button at the bottom left corner in case of Workspace
layout.
.. image:: images/preferences_menu.png
:alt: Preferences menu
:align: center
The left pane of the *Preferences* dialog displays a tree control; each node of
the tree control provides access to options that are related to the node under
which they are displayed.
@ -266,21 +273,23 @@ The Miscellaneous Node
Expand the *Miscellaneous* node to specify miscellaneous display preferences.
.. image:: images/preferences_misc_user_language.png
:alt: Preferences dialog user language section
.. image:: images/preferences_misc_user_interface.png
:alt: Preferences dialog user interface section
:align: center
* Use the *User language* drop-down listbox to select the display language for
* Use the *Language* drop-down listbox to select the display language for
the client.
.. image:: images/preferences_misc_themes.png
:alt: Preferences dialog themes section
:align: center
* Use the *Layout* drop-down listbox to select the layout for the client.
pgAdmin offers two options: the Classic layout, a longstanding and familiar
design, and the Workspace layout, which provides distraction free dedicated
areas for the Query Tool, PSQL, and Schema Diff tools. 'Workspace' layout is
the default layout, but user can change it to 'Classic'.
* Use the *Themes* drop-down listbox to select the theme for pgAdmin. You'll also get a preview just below the
drop down. You can also submit your own themes,
check `here <https://github.com/pgadmin-org/pgadmin4/blob/master/README.md>`_ how.
Currently we support Standard, Dark and High Contrast and System theme. Selecting System option will follow
Currently we support Light, Dark, High Contrast and System theme. Selecting System option will follow
your computer's settings.
The Paths Node

View File

@ -27,4 +27,44 @@ mode, but is disabled by default in Server mode. This is because users can run
arbitrary shell commands through psql which may be considered a security risk in
some deployments. System Administrators can enable the use of the PSQL tool in
the pgAdmin configuration by setting the *ENABLE_PSQL* option to *True*; see
:ref:`config_py` for more information.
:ref:`config_py` for more information.
PSQL Tool in Workspace Layout
******************************
The workspace layout offers a distraction-free, dedicated area for the PSQL Tool.
When the PSQL Tool workspace is accessed, the Welcome page opens by default.
**Note**: In the Workspace layout, all PSQL tabs open within the PSQL Tool workspace.
In the classic UI, users must connect to a database server and navigate to the
database node before using the PSQL Tool. However, with the introduction of the
Workspace layout and Welcome page, users can seamlessly connect to any ad-hoc
server, even if it is not registered in the Object Explorer.
.. image:: images/psql_workspace.png
:alt: PSQL tool workspace
:align: center
* Select *Existing Server* from the dropdown to connect to a server already
listed in the Object Explorer. It is optional.
* Provide the *Server Name* for ad-hoc servers.
* Specify the IP address of the server host, or the fully qualified domain
name in the *Host name/address* field.
* Enter the listener port number of the server host in the *Port* field.
* Use the *Database* field to specify the name of the database to which
the client will connect.
* Use the *User* field to specify the name of a user that will be used when
authenticating with the server.
* Use the *Password* field to provide a password that will be supplied when
authenticating with the server.
* Use the *Role* field to specify the name of a role that has privileges that
will be conveyed to the client after authentication with the server.
* Use the *Service* field to specify the service name. For more information,
see
`Section 33.16 of the Postgres documentation <https://www.postgresql.org/docs/current/libpq-pgservice.html>`_.
* Use the fields in the *Connection Parameters* to configure the connection parameters.
After filling in all the required fields, click the Connect & Open PSQL Tool
button to launch the PSQL Tool with the provided server details. If the password
is not supplied, you will be prompted to enter it.

View File

@ -41,6 +41,47 @@ The Query Tool features two panels:
server messages related to the query's execution and any asynchronous
notifications received from the server.
Query Tool in Workspace Layout
******************************
The workspace layout offers a distraction-free, dedicated area for the Query Tool.
When the Query Tool workspace is accessed, the Welcome page opens by default.
**Note**: In the Workspace layout, all Query Tool and View/Edit Data tabs open within the Query Tool workspace.
In the classic UI, users must connect to a database server and navigate to the
database node before using the Query Tool. However, with the introduction of the
Workspace layout and Welcome page, users can seamlessly connect to any ad-hoc
server, even if it is not registered in the Object Explorer.
.. image:: images/query_tool_workspace.png
:alt: Query tool workspace
:align: center
* Select *Existing Server* from the dropdown to connect to a server already
listed in the Object Explorer. It is optional.
* Provide the *Server Name* for ad-hoc servers.
* Specify the IP address of the server host, or the fully qualified domain
name in the *Host name/address* field.
* Enter the listener port number of the server host in the *Port* field.
* Use the *Database* field to specify the name of the database to which
the client will connect.
* Use the *User* field to specify the name of a user that will be used when
authenticating with the server.
* Use the *Password* field to provide a password that will be supplied when
authenticating with the server.
* Use the *Role* field to specify the name of a role that has privileges that
will be conveyed to the client after authentication with the server.
* Use the *Service* field to specify the service name. For more information,
see
`Section 33.16 of the Postgres documentation <https://www.postgresql.org/docs/current/libpq-pgservice.html>`_.
* Use the fields in the *Connection Parameters* to configure the connection parameters.
After filling in all the required fields, click the Connect & Open Query Tool
button to launch the Query Tool with the provided server details. If the password
is not supplied, you will be prompted to enter it.
Toolbar
*******

View File

@ -12,6 +12,7 @@ notes for it.
:maxdepth: 1
release_notes_9_0
release_notes_8_14
release_notes_8_13
release_notes_8_12

View File

@ -0,0 +1,31 @@
***********
Version 9.0
***********
Release date: 2025-01-09
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v8.14.
Supported Database Servers
**************************
**PostgreSQL**: 13, 14, 15, 16 and 17
**EDB Advanced Server**: 13, 14, 15, 16 and 17
Bundled PostgreSQL Utilities
****************************
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 17.0
New features
************
| `Issue #7708 <https://github.com/pgadmin-org/pgadmin4/issues/7708>`_ - Enhanced pgAdmin 4 with support for Workspace layouts.
Housekeeping
************
Bug fixes
*********

View File

@ -44,6 +44,18 @@ Use the :ref:`Preferences <preferences>` dialog to specify following:
* *Schema Diff* should ignore the whitespaces while comparing string objects. Set *Ignore whitespaces* option to true.
* *Schema Diff* should ignore the owner while comparing objects. Set *Ignore owner* option to true.
Schema Diff in Workspace Layout
*******************************
The workspace layout offers a distraction-free, dedicated area for the Schema Diff.
By default, the Schema Diff workspace button remains disabled until at least one Schema Diff tab is opened.
**Note**: In the Workspace layout, all Schema Diff tabs open within the Schema Diff workspace.
.. image:: images/schema_diff_workspace.png
:alt: schema diff workspace
:align: center
The *Schema Diff* panel is divided into two panels; an Object Comparison panel
and a DDL Comparison panel.

View File

@ -32,7 +32,7 @@ the right pane.
Select an icon from the *Quick Links* panel on the *Dashboard* tab to:
* Click the *Add New Server* button to open the
:ref:`Create - Server dialog <server_dialog>` to add a new server definition.
:ref:`Register - Server dialog <server_dialog>` to add a new server definition.
* Click the *Configure pgAdmin* button to open the
:ref:`Preferences dialog <preferences>` to customize your pgAdmin client.

View File

@ -53,8 +53,7 @@ DEFAULT_BINARY_PATHS = {
'pg-16': '/usr/local/pgsql-16',
'pg-15': '/usr/local/pgsql-15',
'pg-14': '/usr/local/pgsql-14',
'pg-13': '/usr/local/pgsql-13',
'pg-12': '/usr/local/pgsql-12'
'pg-13': '/usr/local/pgsql-13'
}
EOF

View File

@ -458,14 +458,12 @@ STORAGE_DIR = os.path.join(DATA_DIR, 'storage')
##########################################################################
DEFAULT_BINARY_PATHS = {
"pg": "",
"pg-12": "",
"pg-13": "",
"pg-14": "",
"pg-15": "",
"pg-16": "",
"pg-17": "",
"ppas": "",
"ppas-12": "",
"ppas-13": "",
"ppas-14": "",
"ppas-15": "",
@ -480,14 +478,12 @@ DEFAULT_BINARY_PATHS = {
FIXED_BINARY_PATHS = {
"pg": "",
"pg-12": "",
"pg-13": "",
"pg-14": "",
"pg-15": "",
"pg-16": "",
"pg-17": "",
"ppas": "",
"ppas-12": "",
"ppas-13": "",
"ppas-14": "",
"ppas-15": "",

View File

@ -0,0 +1,39 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2024, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""
Revision ID: 255e2842e4d7
Revises: f28be870d5ec
Create Date: 2024-12-05 13:14:53.602974
"""
from alembic import op, context
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '255e2842e4d7'
down_revision = 'f28be870d5ec'
branch_labels = None
depends_on = None
def upgrade():
with (op.batch_alter_table("server",
table_kwargs={'sqlite_autoincrement': True}) as batch_op):
if context.get_impl().bind.dialect.name == "sqlite":
batch_op.alter_column('id', autoincrement=True)
batch_op.add_column(sa.Column('is_adhoc', sa.Integer(),
server_default='0'))
def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass

View File

@ -401,78 +401,82 @@ def create_app(app_name=None):
backup_db_file()
def run_migration_for_sqlite():
with app.app_context():
# Run migration for the first time i.e. create database
# If version not available, user must have aborted. Tables are not
# created and so its an empty db
if not os.path.exists(SQLITE_PATH) or get_version() == -1:
# If running in cli mode then don't try to upgrade, just raise
# the exception
if not cli_mode:
upgrade_db()
else:
if not os.path.exists(SQLITE_PATH):
raise FileNotFoundError(
'SQLite database file "' + SQLITE_PATH +
'" does not exists.')
raise RuntimeError(
'The configuration database file is not valid.')
# Run migration for the first time i.e. create database
# If version not available, user must have aborted. Tables are not
# created and so its an empty db
if not os.path.exists(SQLITE_PATH) or get_version() == -1:
# If running in cli mode then don't try to upgrade, just raise
# the exception
if not cli_mode:
upgrade_db()
else:
schema_version = get_version()
if not os.path.exists(SQLITE_PATH):
raise FileNotFoundError(
'SQLite database file "' + SQLITE_PATH +
'" does not exists.')
raise RuntimeError(
'The configuration database file is not valid.')
else:
schema_version = get_version()
# Run migration if current schema version is greater than the
# schema version stored in version table
if CURRENT_SCHEMA_VERSION > schema_version:
# Take a backup of the old database file.
try:
prev_database_file_name = \
"{0}.prev.bak".format(SQLITE_PATH)
shutil.copyfile(SQLITE_PATH, prev_database_file_name)
except Exception as e:
app.logger.error(e)
# Run migration if current schema version is greater than the
# schema version stored in version table
if CURRENT_SCHEMA_VERSION > schema_version:
# Take a backup of the old database file.
try:
prev_database_file_name = \
"{0}.prev.bak".format(SQLITE_PATH)
shutil.copyfile(SQLITE_PATH, prev_database_file_name)
except Exception as e:
app.logger.error(e)
upgrade_db()
else:
# check all tables are present in the db.
is_db_error, invalid_tb_names = check_db_tables()
if is_db_error:
app.logger.error(
'Table(s) {0} are missing in the'
' database'.format(invalid_tb_names))
backup_db_file()
upgrade_db()
else:
# check all tables are present in the db.
is_db_error, invalid_tb_names = check_db_tables()
if is_db_error:
app.logger.error(
'Table(s) {0} are missing in the'
' database'.format(invalid_tb_names))
backup_db_file()
# Update schema version to the latest
if CURRENT_SCHEMA_VERSION > schema_version:
set_version(CURRENT_SCHEMA_VERSION)
db.session.commit()
# Update schema version to the latest
if CURRENT_SCHEMA_VERSION > schema_version:
set_version(CURRENT_SCHEMA_VERSION)
db.session.commit()
if os.name != 'nt':
os.chmod(config.SQLITE_PATH, 0o600)
if os.name != 'nt':
os.chmod(config.SQLITE_PATH, 0o600)
def run_migration_for_others():
with app.app_context():
# Run migration for the first time i.e. create database
# If version not available, user must have aborted. Tables are not
# created and so its an empty db
if get_version() == -1:
# Run migration for the first time i.e. create database
# If version not available, user must have aborted. Tables are not
# created and so its an empty db
if get_version() == -1:
db_upgrade(app)
else:
schema_version = get_version()
# Run migration if current schema version is greater than
# the schema version stored in version table.
if CURRENT_SCHEMA_VERSION > schema_version:
db_upgrade(app)
else:
schema_version = get_version()
# Update schema version to the latest
set_version(CURRENT_SCHEMA_VERSION)
db.session.commit()
# Run migration if current schema version is greater than
# the schema version stored in version table.
if CURRENT_SCHEMA_VERSION > schema_version:
db_upgrade(app)
# Update schema version to the latest
set_version(CURRENT_SCHEMA_VERSION)
db.session.commit()
from pgadmin.browser.server_groups.servers.utils import (
delete_adhoc_servers)
with app.app_context():
# Run the migration as per specified by the user.
if config.CONFIG_DATABASE_URI is not None and \
len(config.CONFIG_DATABASE_URI) > 0:
run_migration_for_others()
else:
run_migration_for_sqlite()
# Run the migration as per specified by the user.
if config.CONFIG_DATABASE_URI is not None and \
len(config.CONFIG_DATABASE_URI) > 0:
run_migration_for_others()
else:
run_migration_for_sqlite()
# Delete all the adhoc(temporary) servers from the pgAdmin database.
delete_adhoc_servers()
Mail(app)

View File

@ -13,7 +13,7 @@ import React, { useEffect, useState, useRef } from 'react';
import { Box, Grid, InputLabel } from '@mui/material';
import { InputSQL } from '../../../static/js/components/FormComponents';
import getApiInstance from '../../../static/js/api_instance';
import { usePgAdmin } from '../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../static/js/PgAdminProvider';
export default function AboutComponent() {
const containerRef = useRef();

View File

@ -33,7 +33,8 @@ from pgadmin.utils.master_password import get_crypt_key
from pgadmin.utils.exception import CryptKeyMissing
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.browser.server_groups.servers.utils import \
is_valid_ipaddress, get_replication_type
(is_valid_ipaddress, get_replication_type, convert_connection_parameter,
check_ssl_fields)
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
SERVER_CONNECTION_CLOSED
from sqlalchemy import or_
@ -225,7 +226,7 @@ class ServerModule(sg.ServerGroupPluginModule):
hide_shared_server = get_preferences()
servers = Server.query.filter(
or_(Server.user_id == current_user.id, Server.shared),
Server.servergroup_id == gid)
Server.servergroup_id == gid, Server.is_adhoc == 0)
driver = get_driver(PG_DEFAULT_DRIVER)
servers = self.get_servers(servers, hide_shared_server, gid)
@ -464,73 +465,6 @@ class ServerNode(PGChildNodeView):
'clear_saved_password': [{'put': 'clear_saved_password'}],
'clear_sshtunnel_password': [{'put': 'clear_sshtunnel_password'}],
})
SSL_MODES = ['prefer', 'require', 'verify-ca', 'verify-full']
def check_ssl_fields(self, data):
"""
This function will allow us to check and set defaults for
SSL fields
Args:
data: Response data
Returns:
Flag and Data
"""
flag = False
if 'sslmode' in data and data['sslmode'] in self.SSL_MODES:
flag = True
ssl_fields = [
'sslcert', 'sslkey', 'sslrootcert', 'sslcrl', 'sslcompression'
]
# Required SSL fields for SERVER mode from user
required_ssl_fields_server_mode = ['sslcert', 'sslkey']
for field in ssl_fields:
if field in data:
continue
elif config.SERVER_MODE and \
field in required_ssl_fields_server_mode:
# In Server mode,
# we will set dummy SSL certificate file path which will
# prevent using default SSL certificates from web servers
# Set file manager directory from preference
import os
file_extn = '.key' if field.endswith('key') else '.crt'
dummy_ssl_file = os.path.join(
'<STORAGE_DIR>', '.postgresql',
'postgresql' + file_extn
)
data[field] = dummy_ssl_file
# For Desktop mode, we will allow to default
return flag, data
def convert_connection_parameter(self, params):
"""
This function is used to convert the connection parameter based
on the instance type.
"""
conn_params = None
# if params is of type list then it is coming from the frontend,
# and we have to convert it into the dict and store it into the
# database
if isinstance(params, list):
conn_params = {}
for item in params:
conn_params[item['name']] = item['value']
# if params is of type dict then it is coming from the database,
# and we have to convert it into the list of params to show on GUI.
elif isinstance(params, dict):
conn_params = []
for key, value in params.items():
if value is not None:
conn_params.append(
{'name': key, 'keyword': key, 'value': value})
return conn_params
def update_connection_parameter(self, data, server):
"""
@ -600,7 +534,7 @@ class ServerNode(PGChildNodeView):
servers = Server.query.filter(
or_(Server.user_id == current_user.id,
Server.shared),
Server.servergroup_id == gid)
Server.servergroup_id == gid, Server.is_adhoc == 0)
driver = get_driver(PG_DEFAULT_DRIVER)
@ -979,9 +913,9 @@ class ServerNode(PGChildNodeView):
Return list of attributes of all servers.
"""
servers = Server.query.filter(
or_(Server.user_id == current_user.id,
Server.shared),
Server.servergroup_id == gid).order_by(Server.name)
or_(Server.user_id == current_user.id, Server.shared),
Server.servergroup_id == gid,
Server.is_adhoc == 0).order_by(Server.name)
sg = ServerGroup.query.filter_by(
id=gid
).first()
@ -1061,7 +995,7 @@ class ServerNode(PGChildNodeView):
tunnel_authentication = False
tunnel_keep_alive = 0
connection_params = \
self.convert_connection_parameter(server.connection_params)
convert_connection_parameter(server.connection_params)
if server.use_ssh_tunnel:
use_ssh_tunnel = bool(server.use_ssh_tunnel)
@ -1183,7 +1117,7 @@ class ServerNode(PGChildNodeView):
).format(arg)
)
connection_params = self.convert_connection_parameter(
connection_params = convert_connection_parameter(
data.get('connection_params', []))
if 'hostaddr' in connection_params and \
@ -1195,7 +1129,7 @@ class ServerNode(PGChildNodeView):
)
# To check ssl configuration
_, connection_params = self.check_ssl_fields(connection_params)
_, connection_params = check_ssl_fields(connection_params)
# set the connection params again in the data
if 'connection_params' in data:
data['connection_params'] = connection_params
@ -1433,7 +1367,7 @@ class ServerNode(PGChildNodeView):
}
)
def connect(self, gid, sid, is_qt=False):
def connect(self, gid, sid, is_qt=False, server=None):
"""
Connect the Server and return the connection object.
Verification Process before Connection:
@ -1453,8 +1387,12 @@ class ServerNode(PGChildNodeView):
'Connection Request for server#{0}'.format(sid)
)
# Fetch Server Details
server = Server.query.filter_by(id=sid).first()
# In case of Workspace layout ad-hoc server maybe pass to this
# function in that case no need to fetch the server detail based on
# sid.
if server is None:
server = Server.query.filter_by(id=sid).first()
shared_server = None
if server.shared and server.user_id != current_user.id:
shared_server = ServerModule.get_shared_server(server, gid)
@ -1505,7 +1443,6 @@ class ServerNode(PGChildNodeView):
manager.update(server)
conn = manager.connection()
crypt_key = None
# Get enc key
crypt_key_present, crypt_key = get_crypt_key()
if not crypt_key_present:
@ -1571,8 +1508,7 @@ class ServerNode(PGChildNodeView):
# not provided, or password has not been saved earlier.
if prompt_password or prompt_tunnel_password:
return self.get_response_for_password(
server, 428, prompt_password, prompt_tunnel_password
)
server, 428, prompt_password, prompt_tunnel_password)
try:
status, errmsg = conn.connect(
@ -1583,7 +1519,8 @@ class ServerNode(PGChildNodeView):
)
except Exception as e:
return self.get_response_for_password(
server, 401, True, True, getattr(e, 'message', str(e)))
server, 401, True, True,
getattr(e, 'message', str(e)))
if not status:
current_app.logger.error(
@ -1594,8 +1531,7 @@ class ServerNode(PGChildNodeView):
return internal_server_error(errmsg)
return self.get_response_for_password(
server, 401, True, True, errmsg
)
server, 401, True, True, errmsg)
else:
if save_password and config.ALLOW_SAVE_PASSWORD:
try:
@ -1651,6 +1587,8 @@ class ServerNode(PGChildNodeView):
success=1,
info=gettext("Server connected."),
data={
"sid": server.id,
"did": manager.did,
'icon': server_icon_and_background(True, manager, server),
'connected': True,
'server_type': manager.server_type,
@ -2065,6 +2003,7 @@ class ServerNode(PGChildNodeView):
)
else:
data = {
"sid": server.id,
"server_label": server.name,
"username": server.username,
"errmsg": errmsg,
@ -2073,7 +2012,7 @@ class ServerNode(PGChildNodeView):
"allow_save_password":
True if config.ALLOW_SAVE_PASSWORD and
'allow_save_password' in session and
session['allow_save_password'] else False,
session['allow_save_password'] else False
}
return make_json_response(
success=0,

View File

@ -45,6 +45,98 @@ class TagsSchema extends BaseUISchema {
}
}
export function getConnectionParameters() {
return [{
'value': 'hostaddr', 'label': gettext('Host address'), 'vartype': 'string'
}, {
'value': 'passfile', 'label': gettext('Password file'), 'vartype': 'file'
}, {
'value': 'channel_binding', 'label': gettext('Channel binding'), 'vartype': 'enum',
'enumvals': [gettext('prefer'), gettext('require'), gettext('disable')],
'min_server_version': '13'
}, {
'value': 'connect_timeout', 'label': gettext('Connection timeout (seconds)'), 'vartype': 'integer'
}, {
'value': 'client_encoding', 'label': gettext('Client encoding'), 'vartype': 'string'
}, {
'value': 'options', 'label': gettext('Options'), 'vartype': 'string'
}, {
'value': 'application_name', 'label': gettext('Application name'), 'vartype': 'string'
}, {
'value': 'fallback_application_name', 'label': gettext('Fallback application name'), 'vartype': 'string'
}, {
'value': 'keepalives', 'label': gettext('Keepalives'), 'vartype': 'integer'
}, {
'value': 'keepalives_idle', 'label': gettext('Keepalives idle (seconds)'), 'vartype': 'integer'
}, {
'value': 'keepalives_interval', 'label': gettext('Keepalives interval (seconds)'), 'vartype': 'integer'
}, {
'value': 'keepalives_count', 'label': gettext('Keepalives count'), 'vartype': 'integer'
}, {
'value': 'tcp_user_timeout', 'label': gettext('TCP user timeout (milliseconds)'), 'vartype': 'integer',
'min_server_version': '12'
}, {
'value': 'tty', 'label': gettext('TTY'), 'vartype': 'string',
'max_server_version': '13'
}, {
'value': 'replication', 'label': gettext('Replication'), 'vartype': 'enum',
'enumvals': [gettext('on'), gettext('off'), gettext('database')],
'min_server_version': '11'
}, {
'value': 'gssencmode', 'label': gettext('GSS encmode'), 'vartype': 'enum',
'enumvals': [gettext('prefer'), gettext('require'), gettext('disable')],
'min_server_version': '12'
}, {
'value': 'sslmode', 'label': gettext('SSL mode'), 'vartype': 'enum',
'enumvals': [gettext('allow'), gettext('prefer'), gettext('require'),
gettext('disable'), gettext('verify-ca'), gettext('verify-full')]
}, {
'value': 'sslcompression', 'label': gettext('SSL compression?'), 'vartype': 'bool',
}, {
'value': 'sslcert', 'label': gettext('Client certificate'), 'vartype': 'file'
}, {
'value': 'sslkey', 'label': gettext('Client certificate key'), 'vartype': 'file'
}, {
'value': 'sslpassword', 'label': gettext('SSL password'), 'vartype': 'string',
'min_server_version': '13'
}, {
'value': 'sslrootcert', 'label': gettext('Root certificate'), 'vartype': 'file'
}, {
'value': 'sslcrl', 'label': gettext('Certificate revocation list'), 'vartype': 'file',
}, {
'value': 'sslcrldir', 'label': gettext('Certificate revocation list directory'), 'vartype': 'file',
'min_server_version': '14'
}, {
'value': 'sslsni', 'label': gettext('Server name indication'), 'vartype': 'bool',
'min_server_version': '14'
}, {
'value': 'requirepeer', 'label': gettext('Require peer'), 'vartype': 'string',
}, {
'value': 'ssl_min_protocol_version', 'label': gettext('SSL min protocol version'),
'vartype': 'enum', 'min_server_version': '13',
'enumvals': [gettext('TLSv1'), gettext('TLSv1.1'), gettext('TLSv1.2'),
gettext('TLSv1.3')]
}, {
'value': 'ssl_max_protocol_version', 'label': gettext('SSL max protocol version'),
'vartype': 'enum', 'min_server_version': '13',
'enumvals': [gettext('TLSv1'), gettext('TLSv1.1'), gettext('TLSv1.2'),
gettext('TLSv1.3')]
}, {
'value': 'krbsrvname', 'label': gettext('Kerberos service name'), 'vartype': 'string',
}, {
'value': 'gsslib', 'label': gettext('GSS library'), 'vartype': 'string',
}, {
'value': 'target_session_attrs', 'label': gettext('Target session attribute'),
'vartype': 'enum',
'enumvals': [gettext('any'), gettext('read-write'), gettext('read-only'),
gettext('primary'), gettext('standby'), gettext('prefer-standby')]
}, {
'value': 'load_balance_hosts', 'label': gettext('Load balance hosts'),
'vartype': 'enum', 'min_server_version': '16',
'enumvals': [gettext('disable'), gettext('random')]
}];
};
export default class ServerSchema extends BaseUISchema {
constructor(serverGroupOptions=[], userId=0, initValues={}) {
super({
@ -84,7 +176,7 @@ export default class ServerSchema extends BaseUISchema {
});
this.serverGroupOptions = serverGroupOptions;
this.paramSchema = new VariableSchema(this.getConnectionParameters(), null, null, ['name', 'keyword', 'value']);
this.paramSchema = new VariableSchema(getConnectionParameters(), null, null, ['name', 'keyword', 'value']);
this.tagsSchema = new TagsSchema();
this.userId = userId;
_.bindAll(this, 'isShared');
@ -504,96 +596,4 @@ export default class ServerSchema extends BaseUISchema {
}
return false;
}
getConnectionParameters() {
return [{
'value': 'hostaddr', 'label': gettext('Host address'), 'vartype': 'string'
}, {
'value': 'passfile', 'label': gettext('Password file'), 'vartype': 'file'
}, {
'value': 'channel_binding', 'label': gettext('Channel binding'), 'vartype': 'enum',
'enumvals': [gettext('prefer'), gettext('require'), gettext('disable')],
'min_server_version': '13'
}, {
'value': 'connect_timeout', 'label': gettext('Connection timeout (seconds)'), 'vartype': 'integer'
}, {
'value': 'client_encoding', 'label': gettext('Client encoding'), 'vartype': 'string'
}, {
'value': 'options', 'label': gettext('Options'), 'vartype': 'string'
}, {
'value': 'application_name', 'label': gettext('Application name'), 'vartype': 'string'
}, {
'value': 'fallback_application_name', 'label': gettext('Fallback application name'), 'vartype': 'string'
}, {
'value': 'keepalives', 'label': gettext('Keepalives'), 'vartype': 'integer'
}, {
'value': 'keepalives_idle', 'label': gettext('Keepalives idle (seconds)'), 'vartype': 'integer'
}, {
'value': 'keepalives_interval', 'label': gettext('Keepalives interval (seconds)'), 'vartype': 'integer'
}, {
'value': 'keepalives_count', 'label': gettext('Keepalives count'), 'vartype': 'integer'
}, {
'value': 'tcp_user_timeout', 'label': gettext('TCP user timeout (milliseconds)'), 'vartype': 'integer',
'min_server_version': '12'
}, {
'value': 'tty', 'label': gettext('TTY'), 'vartype': 'string',
'max_server_version': '13'
}, {
'value': 'replication', 'label': gettext('Replication'), 'vartype': 'enum',
'enumvals': [gettext('on'), gettext('off'), gettext('database')],
'min_server_version': '11'
}, {
'value': 'gssencmode', 'label': gettext('GSS encmode'), 'vartype': 'enum',
'enumvals': [gettext('prefer'), gettext('require'), gettext('disable')],
'min_server_version': '12'
}, {
'value': 'sslmode', 'label': gettext('SSL mode'), 'vartype': 'enum',
'enumvals': [gettext('allow'), gettext('prefer'), gettext('require'),
gettext('disable'), gettext('verify-ca'), gettext('verify-full')]
}, {
'value': 'sslcompression', 'label': gettext('SSL compression?'), 'vartype': 'bool',
}, {
'value': 'sslcert', 'label': gettext('Client certificate'), 'vartype': 'file'
}, {
'value': 'sslkey', 'label': gettext('Client certificate key'), 'vartype': 'file'
}, {
'value': 'sslpassword', 'label': gettext('SSL password'), 'vartype': 'string',
'min_server_version': '13'
}, {
'value': 'sslrootcert', 'label': gettext('Root certificate'), 'vartype': 'file'
}, {
'value': 'sslcrl', 'label': gettext('Certificate revocation list'), 'vartype': 'file',
}, {
'value': 'sslcrldir', 'label': gettext('Certificate revocation list directory'), 'vartype': 'file',
'min_server_version': '14'
}, {
'value': 'sslsni', 'label': gettext('Server name indication'), 'vartype': 'bool',
'min_server_version': '14'
}, {
'value': 'requirepeer', 'label': gettext('Require peer'), 'vartype': 'string',
}, {
'value': 'ssl_min_protocol_version', 'label': gettext('SSL min protocol version'),
'vartype': 'enum', 'min_server_version': '13',
'enumvals': [gettext('TLSv1'), gettext('TLSv1.1'), gettext('TLSv1.2'),
gettext('TLSv1.3')]
}, {
'value': 'ssl_max_protocol_version', 'label': gettext('SSL max protocol version'),
'vartype': 'enum', 'min_server_version': '13',
'enumvals': [gettext('TLSv1'), gettext('TLSv1.1'), gettext('TLSv1.2'),
gettext('TLSv1.3')]
}, {
'value': 'krbsrvname', 'label': gettext('Kerberos service name'), 'vartype': 'string',
}, {
'value': 'gsslib', 'label': gettext('GSS library'), 'vartype': 'string',
}, {
'value': 'target_session_attrs', 'label': gettext('Target session attribute'),
'vartype': 'enum',
'enumvals': [gettext('any'), gettext('read-write'), gettext('read-only'),
gettext('primary'), gettext('standby'), gettext('prefer-standby')]
}, {
'value': 'load_balance_hosts', 'label': gettext('Load balance hosts'),
'vartype': 'enum', 'min_server_version': '16',
'enumvals': [gettext('disable'), gettext('random')]
}];
}
}

View File

@ -8,21 +8,22 @@
##########################################################################
"""Server helper utilities"""
import config
from ipaddress import ip_address
import keyring
from flask_login import current_user
from werkzeug.exceptions import InternalServerError
from flask import render_template
from pgadmin.utils.constants import KEY_RING_USERNAME_FORMAT, \
KEY_RING_SERVICE_NAME, KEY_RING_USER_NAME, KEY_RING_TUNNEL_FORMAT, \
KEY_RING_DESKTOP_USER
KEY_RING_SERVICE_NAME, KEY_RING_TUNNEL_FORMAT, \
KEY_RING_DESKTOP_USER, SSL_MODES
from pgadmin.utils.crypto import encrypt, decrypt
import config
from pgadmin.model import db, Server
from flask import current_app
from pgadmin.utils.exception import CryptKeyMissing
from pgadmin.utils.master_password import validate_master_password, \
get_crypt_key, set_masterpass_check_text
from pgadmin.utils.master_password import set_masterpass_check_text
from pgadmin.utils.driver import get_driver
from .... import socketio as sio
from sqlalchemy import text
def is_valid_ipaddress(address):
@ -316,7 +317,7 @@ def migrate_passwords_from_pgadmin_db(servers, old_key, enc_key):
def get_servers_with_saved_passwords():
all_server = Server.query.all()
all_server = Server.query.filter(Server.is_adhoc == 0)
servers_with_pwd_in_os_secret = []
servers_with_pwd_in_pgadmin_db = []
saved_password_servers = []
@ -560,3 +561,116 @@ def get_replication_type(conn, sversion):
raise InternalServerError(res)
return res['rows'][0]['type']
def convert_connection_parameter(params):
"""
This function is used to convert the connection parameter based
on the instance type.
"""
conn_params = None
# if params is of type list then it is coming from the frontend,
# and we have to convert it into the dict and store it into the
# database
if isinstance(params, list):
conn_params = {}
for item in params:
conn_params[item['name']] = item['value']
# if params is of type dict then it is coming from the database,
# and we have to convert it into the list of params to show on GUI.
elif isinstance(params, dict):
conn_params = []
for key, value in params.items():
if value is not None:
conn_params.append(
{'name': key, 'keyword': key, 'value': value})
return conn_params
def check_ssl_fields(data):
"""
This function will allow us to check and set defaults for
SSL fields
Args:
data: Response data
Returns:
Flag and Data
"""
flag = False
if 'sslmode' in data and data['sslmode'] in SSL_MODES:
flag = True
ssl_fields = [
'sslcert', 'sslkey', 'sslrootcert', 'sslcrl', 'sslcompression'
]
# Required SSL fields for SERVER mode from user
required_ssl_fields_server_mode = ['sslcert', 'sslkey']
for field in ssl_fields:
if field in data:
continue
elif config.SERVER_MODE and \
field in required_ssl_fields_server_mode:
# In Server mode,
# we will set dummy SSL certificate file path which will
# prevent using default SSL certificates from web servers
# Set file manager directory from preference
import os
file_extn = '.key' if field.endswith('key') else '.crt'
dummy_ssl_file = os.path.join(
'<STORAGE_DIR>', '.postgresql',
'postgresql' + file_extn
)
data[field] = dummy_ssl_file
# For Desktop mode, we will allow to default
return flag, data
def disconnect_from_all_servers():
"""
This function is used to disconnect all the servers
"""
all_servers = Server.query.all()
for server in all_servers:
manager = get_driver(config.PG_DEFAULT_DRIVER).connection_manager(
server.id)
# Check if any psql terminal is running for the current disconnecting
# server. If any terminate the psql tool connection.
if 'sid_soid_mapping' in current_app.config and str(server.id) in \
current_app.config['sid_soid_mapping'] and \
str(server.id) in current_app.config['sid_soid_mapping']:
for i in current_app.config['sid_soid_mapping'][str(server.id)]:
sio.emit('disconnect-psql', namespace='/pty', to=i)
manager.release()
def delete_adhoc_servers():
"""
This function will remove all the adhoc servers.
"""
try:
db.session.query(Server).filter(Server.is_adhoc == 1).delete()
db.session.commit()
# Reset the sequence again
if config.CONFIG_DATABASE_URI is not None and \
len(config.CONFIG_DATABASE_URI) > 0:
query = ("SELECT setval(pg_get_serial_sequence('server', "
"'id'), coalesce(max(id),0) + 1, false) FROM "
"server;")
else:
query = ("UPDATE sqlite_sequence SET seq = "
"(SELECT max(id) from server) WHERE name = "
"'server'")
with db.engine.connect() as connection:
connection.execute(text(query))
connection.commit()
except Exception:
db.session.rollback()
raise

View File

@ -41,7 +41,16 @@ export const BROWSER_PANELS = {
GRANT_WIZARD: 'id-grant-wizard',
SEARCH_OBJECTS: 'id-search-objects',
USER_MANAGEMENT: 'id-user-management',
IMPORT_EXPORT_SERVERS: 'id-import-export-servers'
IMPORT_EXPORT_SERVERS: 'id-import-export-servers',
WELCOME_QUERY_TOOL: 'id-welcome-querytool',
WELCOME_PSQL_TOOL: 'id-welcome-psql'
};
export const WORKSPACES = {
DEFAULT: 'default_workspace',
QUERY_TOOL: 'query_tool_workspace',
PSQL_TOOL: 'psql_workspace',
SCHEMA_DIFF_TOOL: 'schema_diff_workspace'
};
export const WEEKDAYS = [

View File

@ -191,7 +191,7 @@ _.extend(pgBrowser.keyboardNavigation, {
if(combo.key === shortcut_obj.close_tab_panel) {
const panelId = dockLayoutTabs[activeTabIdx].id?.slice(14);
if (panelId) {
pgAdmin.Browser.docker.close(panelId);
pgAdmin.Browser.docker.default_workspace.close(panelId);
}
} else {
if (combo.key === shortcut_obj.tabbed_panel_backward) activeTabIdx = (activeTabIdx + dockLayoutTabs.length - 1) % dockLayoutTabs.length;
@ -291,7 +291,7 @@ _.extend(pgBrowser.keyboardNavigation, {
},
isPropertyPanelVisible: function() {
let isPanelVisible = false;
_.each(pgAdmin.Browser.docker.findPanels(), (panel) => {
_.each(pgAdmin.Browser.docker.default_workspace.findPanels(), (panel) => {
if (panel._type === 'properties')
isPanelVisible = panel.isVisible();
});

View File

@ -382,7 +382,7 @@ define('pgadmin.browser.node', [
treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem);
const panelId = _.uniqueId(BROWSER_PANELS.EDIT_PROPERTIES);
const onClose = (force=false)=>{ pgBrowser.docker.close(panelId, force); };
const onClose = (force=false)=>{ pgBrowser.docker.default_workspace.close(panelId, force); };
const onSave = (newNodeData)=>{
// Clear the cache for this node now.
setTimeout(()=>{
@ -412,7 +412,7 @@ define('pgadmin.browser.node', [
// browser tree upon the 'Save' button click.
treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem);
const panelId = _.uniqueId(BROWSER_PANELS.EDIT_PROPERTIES);
const onClose = (force=false)=>{ pgBrowser.docker.close(panelId, force); };
const onClose = (force=false)=>{ pgBrowser.docker.default_workspace.close(panelId, force); };
const onSave = (newNodeData)=>{
// Clear the cache for this node now.
setTimeout(()=>{
@ -438,7 +438,7 @@ define('pgadmin.browser.node', [
});
} else {
const panelId = BROWSER_PANELS.EDIT_PROPERTIES+nodeData.id;
const onClose = (force=false)=>{ pgBrowser.docker.close(panelId, force); };
const onClose = (force=false)=>{ pgBrowser.docker.default_workspace.close(panelId, force); };
const onSave = (newNodeData)=>{
let _old = nodeData,
_new = newNodeData.node,
@ -466,7 +466,7 @@ define('pgadmin.browser.node', [
);
onClose();
};
if(pgBrowser.docker.find(panelId)) {
if(pgBrowser.docker.default_workspace.find(panelId)) {
let msg = gettext('Are you sure want to stop editing the properties of %s "%s"?');
if (args.action == 'edit') {
msg = gettext('Are you sure want to reset the current changes and re-open the panel for %s "%s"?');
@ -742,6 +742,11 @@ define('pgadmin.browser.node', [
item);
return true;
},
// Callback called - when a node is deselected in browser tree.
deselected: function() {
// The following call disables all menus mapped to any selected tree node.
pgAdmin.Browser.enable_disable_menus.apply(pgBrowser, []);
},
removed: function(item) {
let self = this;
setTimeout(function() {
@ -838,10 +843,10 @@ define('pgadmin.browser.node', [
if(update) {
dialogProps.onClose(true);
setTimeout(()=>{
pgBrowser.docker.openDialog(panelData, w, h);
pgBrowser.docker.default_workspace.openDialog(panelData, w, h);
}, 10);
} else {
pgBrowser.docker.openDialog(panelData, w, h);
pgBrowser.docker.default_workspace.openDialog(panelData, w, h);
}
},
_find_parent_node: function(t, i, d) {

View File

@ -31,7 +31,7 @@ import Memory from './SystemStats/Memory';
import Storage from './SystemStats/Storage';
import withStandardTabInfo from '../../../static/js/helpers/withStandardTabInfo';
import { BROWSER_PANELS } from '../../../browser/static/js/constants';
import { usePgAdmin } from '../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../static/js/PgAdminProvider';
import usePreferences from '../../../preferences/static/js/store';
import ErrorBoundary from '../../../static/js/helpers/ErrorBoundary';
import { parseApiError } from '../../../static/js/api_instance';

View File

@ -18,7 +18,7 @@ import SectionContainer from '../components/SectionContainer';
import ReplicationStatsSchema from './schema_ui/replication_stats.ui';
import RefreshButton from '../components/RefreshButtons';
import { getExpandCell, getSwitchCell } from '../../../../static/js/components/PgReactTableStyled';
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
import url_for from 'sources/url_for';
import PropTypes from 'prop-types';

View File

@ -16,7 +16,7 @@ import getApiInstance, { parseApiError } from '../../../../static/js/api_instanc
import SectionContainer from '../components/SectionContainer';
import RefreshButton from '../components/RefreshButtons';
import { getExpandCell, getSwitchCell } from '../../../../static/js/components/PgReactTableStyled';
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
import url_for from 'sources/url_for';
import PropTypes from 'prop-types';
import PGDOutgoingSchema from './schema_ui/pgd_outgoing.ui';

View File

@ -10,13 +10,12 @@
"""A blueprint module providing utility functions for the application."""
from pgadmin.utils import driver
from flask import render_template, Response, request, current_app
from flask.helpers import url_for
from flask import request, current_app
from flask_babel import gettext
from pgadmin.user_login_check import pga_login_required
from pathlib import Path
from pgadmin.utils import PgAdminModule, replace_binary_path, \
get_binary_path_versions
from pgadmin.utils import PgAdminModule, get_binary_path_versions
from pgadmin.utils.constants import PREF_LABEL_USER_INTERFACE
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.utils.session import cleanup_session_files
from pgadmin.misc.themes import get_all_themes
@ -52,9 +51,9 @@ class MiscModule(PgAdminModule):
# Register options for the User language settings
self.preference.register(
'user_language', 'user_language',
gettext("User language"), 'options', 'en',
category_label=gettext('User language'),
'user_interface', 'user_language',
gettext("Language"), 'options', 'en',
category_label=PREF_LABEL_USER_INTERFACE,
options=lang_options,
control_props={
'allowClear': False,
@ -75,9 +74,9 @@ class MiscModule(PgAdminModule):
})
self.preference.register(
'themes', 'theme',
'user_interface', 'theme',
gettext("Theme"), 'options', 'light',
category_label=gettext('Themes'),
category_label=PREF_LABEL_USER_INTERFACE,
options=theme_options,
control_props={
'allowClear': False,
@ -88,6 +87,24 @@ class MiscModule(PgAdminModule):
'preview of the theme.'
)
)
self.preference.register(
'user_interface', 'layout',
gettext("Layout"), 'options', 'workspace',
category_label=PREF_LABEL_USER_INTERFACE,
options=[{'label': gettext('Classic'), 'value': 'classic'},
{'label': gettext('Workspace'), 'value': 'workspace'}],
control_props={
'allowClear': False,
'creatable': False,
},
help_str=gettext(
'Choose the layout that suits you best. pgAdmin offers two '
'options: the Classic layout, a longstanding and familiar '
'design, and the Workspace layout, which provides distraction '
'free dedicated areas for the Query Tool, PSQL, and Schema '
'Diff tools.'
)
)
def get_exposed_url_endpoints(self):
"""
@ -122,6 +139,9 @@ class MiscModule(PgAdminModule):
from .statistics import blueprint as module
self.submodules.append(module)
from .workspaces import blueprint as module
self.submodules.append(module)
super().register(app, options)

View File

@ -205,11 +205,11 @@ export default class BgProcessManager {
}
openProcessesPanel() {
let processPanel = this.pgBrowser.docker.find(BROWSER_PANELS.PROCESSES);
let processPanel = this.pgBrowser.docker.default_workspace.find(BROWSER_PANELS.PROCESSES);
if(!processPanel) {
pgAdmin.Browser.docker.openTab(processesPanelData, BROWSER_PANELS.MAIN, 'middle', true);
pgAdmin.Browser.docker.default_workspace.openTab(processesPanelData, BROWSER_PANELS.MAIN, 'middle', true);
} else {
this.pgBrowser.docker.focus(BROWSER_PANELS.PROCESSES);
this.pgBrowser.docker.default_workspace.focus(BROWSER_PANELS.PROCESSES);
}
}

View File

@ -20,7 +20,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
import HelpIcon from '@mui/icons-material/HelpRounded';
import url_for from 'sources/url_for';
import { Box } from '@mui/material';
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import ErrorBoundary from '../../../../static/js/helpers/ErrorBoundary';
import ProcessDetails from './ProcessDetails';
@ -163,7 +163,7 @@ export default function Processes() {
const onViewDetailsClick = useCallback((p)=>{
const panelTitle = gettext('Process Watcher - %s', p.type_desc);
const panelId = BROWSER_PANELS.PROCESS_DETAILS+''+p.id;
pgAdmin.Browser.docker.openDialog({
pgAdmin.Browser.docker.default_workspace.openDialog({
id: panelId,
title: panelTitle,
content: (

View File

@ -81,9 +81,9 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanelId}
onClose();
}
};
pgAdmin.Browser.docker.eventBus.registerListener(LAYOUT_EVENTS.CLOSING, onWizardClosing);
pgAdmin.Browser.docker.default_workspace.eventBus.registerListener(LAYOUT_EVENTS.CLOSING, onWizardClosing);
return ()=>{
pgAdmin.Browser.docker.eventBus.deregisterListener(LAYOUT_EVENTS.CLOSING, onWizardClosing);
pgAdmin.Browser.docker.default_workspace.eventBus.deregisterListener(LAYOUT_EVENTS.CLOSING, onWizardClosing);
};
}, []);

View File

@ -79,7 +79,7 @@ define('pgadmin.misc.cloud', [
const panelTitle = gettext('Deploy Cloud Instance');
const panelId = BROWSER_PANELS.CLOUD_WIZARD;
pgAdmin.Browser.docker.openDialog({
pgAdmin.Browser.docker.default_workspace.openDialog({
id: panelId,
title: panelTitle,
manualClose: true,
@ -93,7 +93,7 @@ define('pgadmin.misc.cloud', [
.catch((error) => {
pgAdmin.Browser.notifier.error(gettext(`Error while clearing cloud wizard data: ${error.response.data.errormsg}`));
});
pgAdmin.Browser.docker.close(panelId, true);
pgAdmin.Browser.docker.default_workspace.close(panelId, true);
}}/>
)
}, pgAdmin.Browser.stdW.lg, pgAdmin.Browser.stdH.lg);

View File

@ -20,7 +20,7 @@ import EmptyPanelMessage from '../../../../static/js/components/EmptyPanelMessag
import { parseApiError } from '../../../../static/js/api_instance';
import withStandardTabInfo from '../../../../static/js/helpers/withStandardTabInfo';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
const Root = styled('div')(({theme}) => ({
height : '100%',

View File

@ -20,7 +20,7 @@ import EmptyPanelMessage from '../../../../static/js/components/EmptyPanelMessag
import { parseApiError } from '../../../../static/js/api_instance';
import withStandardTabInfo from '../../../../static/js/helpers/withStandardTabInfo';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
const Root = styled('div')(({theme}) => ({
height : '100%',

View File

@ -21,7 +21,7 @@ import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import EmptyPanelMessage from '../../static/js/components/EmptyPanelMessage';
import Loader from 'sources/components/Loader';
import { evalFunc } from '../../static/js/utils';
import { usePgAdmin } from '../../static/js/BrowserComponent';
import { usePgAdmin } from '../../static/js/PgAdminProvider';
import { getSwitchCell } from '../../static/js/components/PgReactTableStyled';
const StyledBox = styled(Box)(({theme}) => ({

View File

@ -14,7 +14,7 @@ import {getHelpUrl, getEPASHelpUrl} from 'pgadmin.help';
import SchemaView from 'sources/SchemaView';
import gettext from 'sources/gettext';
import { generateNodeUrl } from '../../browser/static/js/node_ajax';
import { usePgAdmin } from '../../static/js/BrowserComponent';
import { usePgAdmin } from '../../static/js/PgAdminProvider';
import { LAYOUT_EVENTS, LayoutDockerContext } from '../../static/js/helpers/Layout';
import usePreferences from '../../preferences/static/js/store';
import PropTypes from 'prop-types';

View File

@ -17,7 +17,7 @@ import ObjectNodeProperties from './ObjectNodeProperties';
import EmptyPanelMessage from '../../static/js/components/EmptyPanelMessage';
import gettext from 'sources/gettext';
import { Box } from '@mui/material';
import { usePgAdmin } from '../../static/js/BrowserComponent';
import { usePgAdmin } from '../../static/js/PgAdminProvider';
import PropTypes from 'prop-types';
import _ from 'lodash';

View File

@ -17,7 +17,7 @@ import CodeMirror from '../../../../static/js/components/ReactCodeMirror';
import Loader from 'sources/components/Loader';
import withStandardTabInfo from '../../../../static/js/helpers/withStandardTabInfo';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
const Root = styled('div')(({theme}) => ({

View File

@ -20,7 +20,7 @@ import EmptyPanelMessage from '../../../../static/js/components/EmptyPanelMessag
import { toPrettySize } from '../../../../static/js/utils';
import withStandardTabInfo from '../../../../static/js/helpers/withStandardTabInfo';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
const Root = styled('div')(({theme}) => ({
height : '100%',

View File

@ -0,0 +1,190 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2024, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""A blueprint module implementing the workspace."""
import json
import config
from flask import request, current_app
from pgadmin.user_login_check import pga_login_required
from flask_babel import gettext
from flask_security import current_user
from pgadmin.utils import PgAdminModule
from pgadmin.model import db, Server
from pgadmin.utils.ajax import bad_request, make_json_response
from pgadmin.browser.server_groups.servers.utils import (
is_valid_ipaddress, convert_connection_parameter, check_ssl_fields)
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.browser.server_groups.servers.utils import (
disconnect_from_all_servers, delete_adhoc_servers)
MODULE_NAME = 'workspace'
class WorkspaceModule(PgAdminModule):
def get_exposed_url_endpoints(self):
"""
Returns:
list: URL endpoints for Workspace module
"""
return [
'workspace.adhoc_connect_server'
]
blueprint = WorkspaceModule(MODULE_NAME, __name__,
url_prefix='/misc/workspace')
@blueprint.route("/")
@pga_login_required
def index():
return bad_request(
errormsg=gettext('This URL cannot be requested directly.')
)
@blueprint.route(
'/adhoc_connect_server',
methods=["POST"],
endpoint="adhoc_connect_server"
)
@pga_login_required
def adhoc_connect_server():
required_args = ['host', 'port', 'user']
data = request.form if request.form else json.loads(
request.data
)
for arg in required_args:
if arg not in data:
return make_json_response(
status=410,
success=0,
errormsg=gettext(
"Could not find the required parameter ({})."
).format(arg)
)
connection_params = convert_connection_parameter(
data.get('connection_params', []))
if 'hostaddr' in connection_params and \
not is_valid_ipaddress(connection_params['hostaddr']):
return make_json_response(
success=0,
status=400,
errormsg=gettext('Not a valid Host address')
)
# To check ssl configuration
_, connection_params = check_ssl_fields(connection_params)
# set the connection params again in the data
if 'connection_params' in data:
data['connection_params'] = connection_params
# Fetch all the new data in case of non-existing servers
new_db = data.get('database_name', None)
if new_db is None:
new_db = data.get('did')
new_username = data.get('user')
new_role = data.get('role', None)
new_server_name = data.get('server_name', None)
try:
server = None
if config.CONFIG_DATABASE_URI is not None and \
len(config.CONFIG_DATABASE_URI) > 0:
# Filter out all the servers with the below combination.
servers = Server.query.filter_by(host=data['host'],
port=data['port'],
maintenance_db=new_db,
username=new_username,
name=new_server_name,
role=new_role
).all()
# If found matching servers then compare the connection_params as
# with external database (PostgreSQL) comparing two json objects
# are not supported.
for existing_server in servers:
if existing_server.connection_params == connection_params:
server = existing_server
break
else:
server = Server.query.filter_by(host=data['host'],
port=data['port'],
maintenance_db=new_db,
username=new_username,
name=new_server_name,
role=new_role,
connection_params=connection_params
).first()
# If server is none then no server with the above combination is found.
if server is None:
# Check if sid is present in data if it is then used that sid.
if ('sid' in data and data['sid'] is not None and
int(data['sid']) > 0):
server = Server.query.filter_by(id=data['sid']).first()
# Clone the server object
server = server.clone()
# Replace the following with the new/changed value.
server.maintenance_db = new_db
server.username = new_username
server.role = new_role
server.connection_params = connection_params
server.is_adhoc = 1
db.session.add(server)
db.session.commit()
else:
server = Server(
user_id=current_user.id,
servergroup_id=data.get('gid', 1),
name=new_server_name,
host=data.get('host', None),
port=data.get('port'),
maintenance_db=new_db,
username=new_username,
role=new_role,
service=data.get('service', None),
connection_params=connection_params,
is_adhoc=1
)
db.session.add(server)
db.session.commit()
view = SchemaDiffRegistry.get_node_view('server')
return view.connect(1, server.id, is_qt=False, server=server)
except Exception as e:
current_app.logger.exception(e)
return make_json_response(
status=410,
success=0,
errormsg=str(e)
)
@blueprint.route(
'/layout_changed',
methods=["DELETE"],
endpoint="layout_changed"
)
@pga_login_required
def layout_changed():
# if layout is changed from 'Workspace' to 'Classic', disconnect all
# servers.
disconnect_from_all_servers()
delete_adhoc_servers()
return make_json_response(status=200)

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 433 KiB

View File

@ -0,0 +1,452 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useMemo, useState } from 'react';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import _ from 'lodash';
import pgWindow from 'sources/window';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import current_user from 'pgadmin.user_management.current_user';
import VariableSchema from '../../../../browser/server_groups/servers/static/js/variable.ui';
import { getConnectionParameters } from '../../../../browser/server_groups/servers/static/js/server.ui';
import { flattenSelectOptions } from '../../../../static/js/components/FormComponents';
import ConnectServerContent from '../../../../static/js/Dialogs/ConnectServerContent';
import SchemaView from '../../../../static/js/SchemaView';
import PropTypes from 'prop-types';
import getApiInstance from '../../../../static/js/api_instance';
import { useModal } from '../../../../static/js/helpers/ModalProvider';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
import * as commonUtils from 'sources/utils';
import * as showQueryTool from '../../../../tools/sqleditor/static/js/show_query_tool';
import { getTitle, generateTitle } from '../../../../tools/sqleditor/static/js/sqleditor_title';
import usePreferences from '../../../../preferences/static/js/store';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
class AdHocConnectionSchema extends BaseUISchema {
constructor(connectExistingServer, initValues={}) {
super({
sid: null,
did: null,
user: null,
server_name: null,
database_name: null,
connected: false,
host: '',
port: undefined,
username: current_user.name,
role: null,
password: undefined,
service: undefined,
connection_params: [
{'name': 'sslmode', 'value': 'prefer', 'keyword': 'sslmode'},
{'name': 'connect_timeout', 'value': 10, 'keyword': 'connect_timeout'}],
...initValues,
});
this.flatServers = [];
this.groupedServers = [];
this.dbs = [];
this.api = getApiInstance();
this.connectExistingServer = connectExistingServer;
this.paramSchema = new VariableSchema(getConnectionParameters, null, null, ['name', 'keyword', 'value']);
}
setServerConnected(sid, icon) {
for(const group of this.groupedServers) {
for(const opt of group.options) {
if(opt.value == sid) {
opt.connected = true;
opt.image = icon || 'icon-pg';
break;
}
}
}
}
isServerConnected(sid) {
return _.find(this.flatServers, (s) => s.value == sid)?.connected;
}
getServerList() {
if(this.groupedServers?.length != 0) {
return Promise.resolve(this.groupedServers);
}
return new Promise((resolve, reject)=>{
this.api.get(url_for('sqleditor.get_new_connection_servers'))
.then(({data: respData})=>{
let groupedOptions = [];
_.forIn(respData.data.result.server_list, (v, k)=>{
if(v.length == 0) {
return;
}
groupedOptions.push({
label: k,
options: v,
});
});
/* Will be re-used for changing icon when connected */
this.groupedServers = groupedOptions.map((group)=>{
return {
label: group.label,
options: group.options.map((o)=>({...o, selected: false})),
};
});
resolve(groupedOptions);
})
.catch((error)=>{
reject(error instanceof Error ? error : Error(gettext('Something went wrong')));
});
});
}
getOtherOptions(sid, type) {
if(!sid) {
return [];
}
if(!this.isServerConnected(sid)) {
return [];
}
return new Promise((resolve, reject)=>{
this.api.get(url_for(`sqleditor.${type}`, {
'sid': sid,
'sgid': 0,
}))
.then(({data: respData})=>{
resolve(respData.data.result.data);
})
.catch((error)=>{
reject(error instanceof Error ? error : Error(gettext('Something went wrong')));
});
});
}
get baseFields() {
let self = this;
return [
{
id: 'sid', label: gettext('Existing Server (Optional)'), deps: ['connected'],
type: () => ({
type: 'select',
options: () => self.getServerList(),
optionsLoaded: (res) => self.flatServers = flattenSelectOptions(res),
optionsReloadBasis: self.flatServers.map((s) => s.connected).join(''),
}),
depChange: (state)=>{
/* Once the option is selected get the name */
/* Force sid to null, and set only if connected */
let selectedServer = _.find(
self.flatServers, (s) => s.value == state.sid
);
return {
server_name: selectedServer?.label,
did: null,
user: null,
role: null,
sid: null,
host: selectedServer?.host,
port: selectedServer?.port,
service: selectedServer?.service,
connection_params: selectedServer?.connection_params,
connected: selectedServer?.connected
};
},
deferredDepChange: (state, source, topState, actionObj) => {
return new Promise((resolve) => {
let sid = actionObj.value;
let selectedServer = _.find(self.flatServers, (s)=>s.value==sid);
if(sid && !_.find(self.flatServers, (s) => s.value == sid)?.connected) {
this.connectExistingServer(sid, state.user, null, (data) => {
self.setServerConnected(sid, data.icon);
resolve(() => ({ sid: sid, host: selectedServer?.host,
port: selectedServer?.port, service: selectedServer?.service,
connection_params: selectedServer?.connection_params, connected: true
}));
});
} else {
resolve(()=>({ sid: sid, host: selectedServer?.host,
port: selectedServer?.port, service: selectedServer?.service,
connection_params: selectedServer?.connection_params, connected: true
}));
}
});
},
},
{
id: 'server_name', label: gettext('Server Name'), type: 'text', noEmpty: true,
deps: ['sid', 'connected'],
disabled: (state) => state.sid,
}, {
id: 'host', label: gettext('Host name/address'), type: 'text', noEmpty: true,
deps: ['sid', 'connected'],
disabled: (state) => state.sid,
}, {
id: 'port', label: gettext('Port'), type: 'int', min: 1, max: 65535, noEmpty: true,
deps: ['sid', 'connected'],
disabled: (state) => state.sid,
},{
id: 'did', label: gettext('Database'), deps: ['sid', 'connected'],
noEmpty: true, controlProps: {creatable: true},
type: (state) => {
if (state?.sid) {
return {
type: 'select',
options: () => this.getOtherOptions(
state.sid, 'get_new_connection_database'
),
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
};
} else {
return {type: 'text'};
}
},
optionsLoaded: (res) => this.dbs = res,
depChange: (state) => {
/* Once the option is selected get the name */
return {
database_name: _.find(this.dbs, (s) => s.value == state.did)?.label
};
}
}, {
id: 'user', label: gettext('User'), deps: ['sid', 'connected'],
noEmpty: true, controlProps: {creatable: true},
type: (state) => {
if (state?.sid) {
return {
type: 'select',
options: () => this.getOtherOptions(
state.sid, 'get_new_connection_user'
),
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
};
} else {
return {type: 'text'};
}
},
}, {
id: 'password', label: gettext('Password'), type: 'password',
controlProps: {
maxLength: null,
autoComplete: 'new-password'
},
deps: ['sid', 'connected'],
},{
id: 'role', label: gettext('Role'), deps: ['sid', 'connected'],
controlProps: {creatable: true},
type: (state)=>({
type: 'select',
options: () => this.getOtherOptions(
state.sid, 'get_new_connection_role'
),
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
}),
},{
id: 'service', label: gettext('Service'), type: 'text', deps: ['sid', 'connected'],
disabled: (state) => state.sid,
}, {
id: 'connection_params', label: gettext('Connection Parameters'),
type: 'collection',
schema: this.paramSchema, mode: ['edit', 'create'], uniqueCol: ['name'],
canAdd: true, canEdit: false, canDelete: true,
}, {
id: 'connected', label: '', type: 'text', visible: false,
}, {
id: 'database_name', label: '', type: 'text', visible: false,
}
];
}
}
export default function AdHocConnection({mode}) {
const [connecting, setConnecting] = useState(false);
const api = getApiInstance();
const modal = useModal();
const pgAdmin = usePgAdmin();
const preferencesStore = usePreferences();
const connectExistingServer = async (sid, user, formData, connectCallback) => {
setConnecting(true);
try {
let {data: respData} = await api({
method: 'POST',
url: url_for('sqleditor.connect_server', {
'sid': sid,
...(user ? {
'usr': user,
}:{}),
}),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: formData
});
setConnecting(false);
connectCallback?.(respData.data);
} catch (error) {
if(!error.response) {
pgAdmin.Browser.notifier.pgNotifier('error', error, 'Connection error', gettext('Connection to pgAdmin server has been lost.'));
} else {
modal.showModal(gettext('Connect to server'), (closeModal)=>{
return (
<ConnectServerContent
closeModal={()=>{
setConnecting(false);
closeModal();
}}
data={error.response?.data?.result}
onOK={(formData)=>{
connectExistingServer(sid, null, formData, connectCallback);
}}
hideSavePassword={true}
/>
);
});
}
}
};
const openQueryTool = (respData, formData)=>{
const transId = commonUtils.getRandomInt(1, 9999999);
let db_name = _.isNil(formData.database_name) ? formData.did : formData.database_name;
let parentData = {
server_group: {_id: 1},
server: {
_id: respData.data.sid,
server_type: respData.data.server_type,
},
database: {
_id: respData.data.did,
label: db_name,
_label: db_name,
},
};
const gridUrl = showQueryTool.generateUrl(transId, parentData, null);
const title = getTitle(pgAdmin, preferencesStore.getPreferencesForModule('browser'), null, false, formData.server_name, db_name, formData.role || formData.user);
showQueryTool.launchQueryTool(pgWindow.pgAdmin.Tools.SQLEditor, transId, gridUrl, title, {
user: formData.user,
role: formData.role,
});
};
const openPSQLTool = (respData, formData)=> {
const transId = commonUtils.getRandomInt(1, 9999999);
let db_name = _.isNil(formData.database_name) ? formData.did : formData.database_name;
let panelTitle = '';
// Set psql tab title as per prefrences setting.
let title_data = {
'database': db_name ? _.unescape(db_name) : 'postgres' ,
'username': formData.user,
'server': formData.server_name,
'type': 'psql_tool',
};
let tab_title_placeholder = usePreferences.getState().getPreferencesForModule('browser').psql_tab_title_placeholder;
panelTitle = generateTitle(tab_title_placeholder, title_data);
let openUrl = url_for('psql.panel', {
trans_id: transId,
});
const misc_preferences = usePreferences.getState().getPreferencesForModule('misc');
let theme = misc_preferences.theme;
openUrl += `?sgid=${1}`
+`&sid=${respData.data.sid}`
+`&did=${respData.data.did}`
+`&server_type=${respData.data.server_type}`
+ `&theme=${theme}`;
if(formData.did) {
openUrl += `&db=${encodeURIComponent(db_name)}`;
} else {
openUrl += `&db=${''}`;
}
const escapedTitle = _.escape(panelTitle);
const open_new_tab = usePreferences.getState().getPreferencesForModule('browser').new_browser_tab_open;
pgAdmin.Browser.Events.trigger(
'pgadmin:tool:show',
`${BROWSER_PANELS.PSQL_TOOL}_${transId}`,
openUrl,
{title: escapedTitle, db: db_name},
{title: panelTitle, icon: 'pg-font-icon icon-terminal', manualClose: false, renamable: true},
Boolean(open_new_tab?.includes('psql_tool'))
);
return true;
};
const onSaveClick = async (isNew, formData) => {
try {
let {data: respData} = await api({
method: 'POST',
url: url_for('workspace.adhoc_connect_server'),
data: JSON.stringify(formData)
});
if (mode == 'Query Tool') {
openQueryTool(respData, formData);
} else if (mode == 'PSQL') {
openPSQLTool(respData, formData);
}
} catch (error) {
if(!error.response) {
pgAdmin.Browser.notifier.pgNotifier('error', error, 'Connection error', gettext('Connect to server.'));
} else {
formData['sid'] = error.response?.data?.result?.sid;
modal.showModal(gettext('Connect to server'), (closeModal)=>{
return (
<ConnectServerContent
closeModal={()=>{
closeModal();
}}
data={error.response?.data?.result}
onOK={(okFormData)=>{
formData['password'] = okFormData.get('password');
onSaveClick(isNew, formData);
}}
hideSavePassword={true}
/>
);
});
}
}
};
let saveBtnName = gettext('Connect & Open Query Tool');
if (mode == 'PSQL') {
saveBtnName = gettext('Connect & Open PSQL');
}
let adHocConObj = useMemo(() => new AdHocConnectionSchema(connectExistingServer), []);
return <SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
formClassName={'AdHocConnection-container'}
schema={adHocConObj}
viewHelperProps={{
mode: 'create',
}}
loadingText={connecting ? 'Connecting...' : ''}
onSave={onSaveClick}
customSaveBtnName= {saveBtnName}
customCloseBtnName={''}
customSaveBtnIconType={mode}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
isTabView={false}
/>;
}
AdHocConnection.propTypes = {
mode: PropTypes.string
};

View File

@ -0,0 +1,113 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useContext, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { WORKSPACES } from '../../../../browser/static/js/constants';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
import usePreferences from '../../../../preferences/static/js/store';
import { config } from './config';
const WorkspaceContext = React.createContext();
export const useWorkspace = ()=>useContext(WorkspaceContext);
export function WorkspaceProvider({children}) {
const pgAdmin = usePgAdmin();
const [currentWorkspace, setCurrentWorkspace] = useState(WORKSPACES.DEFAULT);
const lastSelectedTreeItem = useRef();
const isClassic = (usePreferences()?.getPreferencesForModule('misc')?.layout ?? 'classic') == 'classic';
/* In case of classic UI all workspace objects should point to the
* the instance of the default layout.
*/
if (isClassic && pgAdmin.Browser.docker.default_workspace) {
pgAdmin.Browser.docker.query_tool_workspace = pgAdmin.Browser.docker.default_workspace;
pgAdmin.Browser.docker.psql_workspace = pgAdmin.Browser.docker.default_workspace;
pgAdmin.Browser.docker.schema_diff_workspace = pgAdmin.Browser.docker.default_workspace;
}
pgAdmin.Browser.getDockerHandler = (panelId)=>{
let docker;
let workspace;
if (isClassic) {
return undefined;
}
const wsConfig = config.find((i)=>panelId.indexOf(i.panel)>=0);
if (wsConfig) {
docker = pgAdmin.Browser.docker[wsConfig.docker];
workspace = wsConfig.workspace;
} else {
docker = pgAdmin.Browser.docker.default_workspace;
workspace = WORKSPACES.DEFAULT;
}
// Call onWorkspaceChange to enable or disable the menu based on the selected workspace.
changeWorkspace(workspace);
return {docker: docker, focus: ()=>changeWorkspace(workspace)};
};
const changeWorkspace = (newVal)=>{
// Set the currentWorkspace flag.
pgAdmin.Browser.docker.currentWorkspace = newVal;
if (newVal == WORKSPACES.DEFAULT) {
setTimeout(() => {
pgAdmin.Browser.tree.selectNode(lastSelectedTreeItem.current);
lastSelectedTreeItem.current = null;
}, 0);
} else {
// Get the selected tree node and save it into the state variable.
let selItem = pgAdmin.Browser.tree.selected();
if (selItem)
lastSelectedTreeItem.current = selItem;
// Deselect the node to disable the menu options.
pgAdmin.Browser.tree.deselect(selItem);
}
setCurrentWorkspace(newVal);
};
const hasOpenTabs = (forWs)=>{
const wsConfig = config.find((i)=>i.workspace == forWs);
if(wsConfig) {
return Boolean(pgAdmin.Browser.docker[wsConfig.docker]?.layoutObj?.getRootElement().querySelector('.dock-tab'));
}
return true;
};
const getLayoutObj = (forWs)=>{
const wsConfig = config.find((i)=>i.workspace == forWs);
if(wsConfig) {
return pgAdmin.Browser.docker[wsConfig.docker];
}
return pgAdmin.Browser.docker.default_workspace;
};
const onWorkspaceDisabled = ()=>{
changeWorkspace(WORKSPACES.DEFAULT);
};
const value = useMemo(()=>({
config: config,
currentWorkspace: currentWorkspace,
enabled: !isClassic,
changeWorkspace,
hasOpenTabs,
getLayoutObj,
onWorkspaceDisabled
}), [currentWorkspace, isClassic]);
return <WorkspaceContext.Provider value={value}>
{children}
</WorkspaceContext.Provider>;
}
WorkspaceProvider.propTypes = {
children: PropTypes.array
};

View File

@ -0,0 +1,121 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useState } from 'react';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
import { Box } from '@mui/material';
import { QueryToolIcon, SchemaDiffIcon } from '../../../../static/js/components/ExternalIcon';
import TerminalRoundedIcon from '@mui/icons-material/TerminalRounded';
import SettingsIcon from '@mui/icons-material/Settings';
import AccountTreeRoundedIcon from '@mui/icons-material/AccountTreeRounded';
import { PgIconButton } from '../../../../static/js/components/Buttons';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import { WORKSPACES } from '../../../../browser/static/js/constants';
import { useWorkspace } from './WorkspaceProvider';
import { LAYOUT_EVENTS } from '../../../../static/js/helpers/Layout';
const StyledWorkspaceButton = styled(PgIconButton)(({theme}) => ({
'&.Buttons-iconButtonDefault': {
border: 'none',
borderRight: '2px solid transparent' ,
borderRadius: 0,
padding: '8px 6px',
height: '40px',
'&.active': {
borderRightColor: theme.otherVars.activeBorder,
},
'&.Mui-disabled': {
borderRightColor: 'transparent',
}
},
}));
function WorkspaceButton({menuItem, value, ...props}) {
const {currentWorkspace, hasOpenTabs, getLayoutObj, onWorkspaceDisabled, changeWorkspace} = useWorkspace();
const active = value == currentWorkspace;
const [disabled, setDisabled] = useState();
useEffect(()=>{
const layout = getLayoutObj(value);
const deregInit = layout.eventBus.registerListener(LAYOUT_EVENTS.INIT, ()=>{
setDisabled(!hasOpenTabs(value));
});
const deregChange = layout.eventBus.registerListener(LAYOUT_EVENTS.CHANGE, ()=>{
setDisabled(!hasOpenTabs(value));
});
const deregRemove = layout.eventBus.registerListener(LAYOUT_EVENTS.REMOVE, ()=>{
setDisabled(!hasOpenTabs(value));
});
return ()=>{
deregInit();
deregChange();
deregRemove();
};
}, []);
useEffect(()=>{
if(disabled && active) {
onWorkspaceDisabled();
}
}, [disabled]);
return (
<StyledWorkspaceButton className={active ? 'active': ''} title={menuItem?.label??''} {...props}
onClick={()=>{
if(menuItem) {
menuItem?.callback();
} else {
changeWorkspace(value);
}
}}
disabled={disabled}
/>
);
}
WorkspaceButton.propTypes = {
menuItem: PropTypes.object,
active: PropTypes.bool,
changeWorkspace: PropTypes.func,
value: PropTypes.string
};
export default function WorkspaceToolbar() {
const [menus, setMenus] = useState({
'settings': undefined,
});
const pgAdmin = usePgAdmin();
const checkMenuState = ()=>{
const fileMenus = pgAdmin.Browser.MainMenus.
find((m)=>(m.name=='file'))?.
menuItems;
setMenus({
'settings': fileMenus?.find((m)=>(m.name=='mnu_preferences')),
});
};
useEffect(()=>{
checkMenuState();
}, []);
return (
<Box style={{borderTop: '1px solid #dde0e6', borderRight: '1px solid #dde0e6'}} display="flex" flexDirection="column" alignItems="center" gap="2px">
<WorkspaceButton icon={<AccountTreeRoundedIcon />} value={WORKSPACES.DEFAULT} />
<WorkspaceButton icon={<QueryToolIcon />} value={WORKSPACES.QUERY_TOOL} />
<WorkspaceButton icon={<TerminalRoundedIcon style={{height: '1.4rem'}}/>} value={WORKSPACES.PSQL_TOOL} />
<WorkspaceButton icon={<SchemaDiffIcon />} value={WORKSPACES.SCHEMA_DIFF_TOOL} />
<Box marginTop="auto">
<WorkspaceButton icon={<SettingsIcon />} menuItem={menus['settings']} />
</Box>
</Box>
);
}

View File

@ -0,0 +1,130 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { styled } from '@mui/material/styles';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
import { Box } from '@mui/material';
import AdHocConnection from './AdHocConnection';
import WelcomeBG from '../img/welcome_background.svg?svgr';
import { QueryToolIcon } from '../../../../static/js/components/ExternalIcon';
import TerminalRoundedIcon from '@mui/icons-material/TerminalRounded';
import { renderToStaticMarkup } from 'react-dom/server';
const welcomeBackgroundString = encodeURIComponent(renderToStaticMarkup(<WelcomeBG />));
const welcomeBackgroundURI = `url("data:image/svg+xml,${welcomeBackgroundString}")`;
const Root = styled('div')(({theme}) => ({
height: '100%',
display: 'flex',
backgroundColor: theme.otherVars.emptySpaceBg,
'& .WorkspaceWelcomePage-content': {
position: 'relative',
overflow: 'hidden',
display: 'flex',
flexGrow: 1,
maxWidth: '900px',
margin:'auto',
zIndex: 1,
maxHeight: '80%',
height: '100%',
'& .AdHocConnection-container.FormView-nonTabPanel': {backgroundColor: theme.palette.background.default}
},
'& .LeftContainer': {
maxWidth: '30%',
padding: '32px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
backgroundColor: theme.palette.grey[200],
opacity: '0.9'
},
'& .RightContainer': {
width: '100%',
padding: '8px',
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default
},
'& .ToolIcon': {
color: theme.palette.primary['main']
},
'& .TitleStyle': {
fontSize: 'medium',
fontWeight: 'bold',
paddingTop: '16px'
},
'& .TopLabelStyle': {
fontSize: 'medium',
fontWeight: 'bold',
padding: '16px 0px 16px 12px'
}
}));
const BackgroundSVG = styled(Box)(() => ({
position: 'absolute',
top: 0,
bottom: 0,
margin: 'auto',
right: 0,
background: welcomeBackgroundURI,
width: '100%',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center'
}));
export default function WorkspaceWelcomePage({ mode }) {
let welcomeIcon = <QueryToolIcon style={{height: '1.5rem'}} />;
let welcomeTitle = gettext('Welcome to the Query Tool Workspace!');
let welcomeFirst = gettext('The Query Tool is a robust and versatile environment designed for executing SQL commands and reviewing result sets efficiently.');
let welcomeSecond = gettext('In this workspace, you can seamlessly open and manage multiple query tabs, making it easier to organize your work. You can select the existing servers or create a completely new ad-hoc connection to any database server as needed.');
if (mode == 'PSQL') {
welcomeIcon = <TerminalRoundedIcon style={{height: '2rem', width: 'unset'}} />;
welcomeTitle = gettext('Welcome to the PSQL Workspace!');
welcomeFirst = gettext('The PSQL tool allows users to connect to PostgreSQL or EDB Advanced server using the psql command line interface.');
welcomeSecond = gettext('In this workspace, you can seamlessly open and manage multiple PSQL tabs, making it easier to organize your work. You can select the existing servers or create a completely new ad-hoc connection to any database server as needed.');
}
return (
<Root>
<BackgroundSVG />
<Box className='WorkspaceWelcomePage-content'>
<Box className='LeftContainer'>
<div className='ToolIcon'>{welcomeIcon}</div>
<Box className='TitleStyle'>
{welcomeTitle}
</Box>
<Box style={{paddingTop: '16px'}}>
{welcomeFirst}
</Box>
<Box style={{paddingTop: '16px'}}>
{welcomeSecond}
</Box>
</Box>
<Box className='RightContainer'>
<Box className='TopLabelStyle'>
{gettext('Let\'s connect to the server')}
</Box>
<AdHocConnection mode={mode}/>
</Box>
</Box>
</Root>
);
}
WorkspaceWelcomePage.propTypes = {
mode: PropTypes.string
};

View File

@ -0,0 +1,96 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import { BROWSER_PANELS, WORKSPACES } from '../../../../browser/static/js/constants';
import WorkspaceWelcomePage from './WorkspaceWelcomePage';
import React from 'react';
const welcomeQueryToolPanelData = {
id: BROWSER_PANELS.WELCOME_QUERY_TOOL, title: gettext('Welcome'), content: <WorkspaceWelcomePage mode={'Query Tool'} />, closable: false, group: 'playground'
};
const welcomePSQLPanelData = {
id: BROWSER_PANELS.WELCOME_PSQL_TOOL, title: gettext('Welcome'), content: <WorkspaceWelcomePage mode={'PSQL'} />, closable: false, group: 'playground'
};
export const config = [
{
docker: 'query_tool_workspace',
panel: BROWSER_PANELS.QUERY_TOOL,
workspace: WORKSPACES.QUERY_TOOL,
layout: {
dockbox: {
mode: 'vertical',
children: [
{
mode: 'horizontal',
children: [
{
size: 100,
id: BROWSER_PANELS.MAIN,
group: 'playground',
tabs: [welcomeQueryToolPanelData],
panelLock: {panelStyle: 'playground'},
}
]
},
]
}
}
},
{
docker: 'psql_workspace',
panel: BROWSER_PANELS.PSQL_TOOL,
workspace: WORKSPACES.PSQL_TOOL,
layout: {
dockbox: {
mode: 'vertical',
children: [
{
mode: 'horizontal',
children: [
{
size: 100,
id: BROWSER_PANELS.MAIN,
group: 'playground',
tabs: [welcomePSQLPanelData],
panelLock: {panelStyle: 'playground'},
}
]
},
]
}
}
},
{
docker: 'schema_diff_workspace',
panel: BROWSER_PANELS.SCHEMA_DIFF_TOOL,
workspace: WORKSPACES.SCHEMA_DIFF_TOOL,
layout: {
dockbox: {
mode: 'vertical',
children: [
{
mode: 'horizontal',
children: [
{
size: 100,
id: BROWSER_PANELS.MAIN,
group: 'playground',
tabs: [],
panelLock: {panelStyle: 'playground'},
}
]
},
]
}
}
},
];

View File

@ -33,7 +33,7 @@ import config
#
##########################################################################
SCHEMA_VERSION = 41
SCHEMA_VERSION = 42
##########################################################################
#
@ -210,6 +210,18 @@ class Server(db.Model):
connection_params = db.Column(MutableDict.as_mutable(types.JSON))
prepare_threshold = db.Column(db.Integer(), nullable=True)
tags = db.Column(types.JSON)
is_adhoc = db.Column(
db.Integer(),
db.CheckConstraint('is_adhoc >= 0 AND is_adhoc <= 1'),
nullable=False, default=0
)
def clone(self):
d = dict(self.__dict__)
d.pop("id") # get rid of id
d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr
copy = self.__class__(**d)
return copy
class ModulePreference(db.Model):

View File

@ -14,7 +14,7 @@ side and for getting/setting preferences.
import config
import json
from flask import render_template, url_for, Response, request, session
from flask import render_template, Response, request, session, current_app
from flask_babel import gettext
from pgadmin.user_login_check import pga_login_required
from pgadmin.utils import PgAdminModule

View File

@ -533,7 +533,28 @@ export default function PreferencesComponent({ ...props }) {
}
if (_data.length > 0) {
save(_data, data);
// Check whether layout is changed from Workspace to Classic.
let layoutPref = _data.find(x => x.name === 'layout');
// If layout is changed then raise the warning to close all the connections.
if (!_.isUndefined(layoutPref) && layoutPref.value == 'classic') {
pgAdmin.Browser.notifier.confirm(
gettext('Layout changed'),
`${gettext('Switching from Workspace to Classic layout will disconnect all server connections and refresh the entire page.')}
${gettext('To avoid losing unsaved data, click Cancel to manually review and close your connections.')}
${gettext('Note that if you choose Cancel, any changes to your preferences will not be saved.')}<br><br>
${gettext('Do you want to continue?')}`,
function () {
save(_data, data, true);
},
function () {
return true;
},
gettext('Continue'),
gettext('Cancel')
);
} else {
save(_data, data);
}
}
}
@ -546,62 +567,80 @@ export default function PreferencesComponent({ ...props }) {
return requires_refresh;
}
function save(save_data, data) {
function save(save_data, data, layout_changed=false) {
api({
url: url_for('preferences.index'),
method: 'PUT',
data: save_data,
}).then(() => {
let requiresTreeRefresh = save_data.some((s)=>{
return (
s.name=='show_system_objects' || s.name=='show_empty_coll_nodes' ||
s.name.startsWith('show_node_') || s.name=='hide_shared_server' ||
s.name=='show_user_defined_templates'
);
});
let requires_refresh = false;
for (const [key] of Object.entries(data.current)) {
let pref = preferencesStore.getPreferenceForId(Number(key));
requires_refresh = checkRefreshRequired(pref, requires_refresh);
}
// If layout is changed then only refresh the object explorer.
if (layout_changed) {
api({
url: url_for('workspace.layout_changed'),
method: 'DELETE',
data: save_data,
}).then(() => {
pgAdmin.Browser.tree.destroy().then(
() => {
pgAdmin.Browser.Events.trigger(
'pgadmin-browser:tree:destroyed', undefined, undefined
);
return true;
}
);
});
} else {
let requiresTreeRefresh = save_data.some((s)=>{
return (
s.name=='show_system_objects' || s.name=='show_empty_coll_nodes' ||
s.name.startsWith('show_node_') || s.name=='hide_shared_server' ||
s.name=='show_user_defined_templates'
);
});
let requires_refresh = false;
for (const [key] of Object.entries(data.current)) {
let pref = preferencesStore.getPreferenceForId(Number(key));
requires_refresh = checkRefreshRequired(pref, requires_refresh);
}
if (requiresTreeRefresh) {
pgAdmin.Browser.notifier.confirm(
gettext('Object explorer refresh required'),
gettext(
'An object explorer refresh is required. Do you wish to refresh it now?'
),
function () {
pgAdmin.Browser.tree.destroy().then(
() => {
pgAdmin.Browser.Events.trigger(
'pgadmin-browser:tree:destroyed', undefined, undefined
);
return true;
}
);
},
function () {
return true;
},
gettext('Refresh'),
gettext('Later')
);
}
if (requiresTreeRefresh) {
pgAdmin.Browser.notifier.confirm(
gettext('Object explorer refresh required'),
gettext(
'An object explorer refresh is required. Do you wish to refresh it now?'
),
function () {
pgAdmin.Browser.tree.destroy().then(
() => {
pgAdmin.Browser.Events.trigger(
'pgadmin-browser:tree:destroyed', undefined, undefined
);
return true;
}
);
},
function () {
return true;
},
gettext('Refresh'),
gettext('Later')
);
}
if (requires_refresh) {
pgAdmin.Browser.notifier.confirm(
gettext('Refresh required'),
gettext('A page refresh is required to apply the theme. Do you wish to refresh the page now?'),
function () {
/* If user clicks Yes */
reloadPgAdmin();
return true;
},
function () { props.closeModal();},
gettext('Refresh'),
gettext('Later')
);
if (requires_refresh) {
pgAdmin.Browser.notifier.confirm(
gettext('Refresh required'),
gettext('A page refresh is required. Do you wish to refresh the page now?'),
function () {
/* If user clicks Yes */
reloadPgAdmin();
return true;
},
function () { props.closeModal();},
gettext('Refresh'),
gettext('Later')
);
}
}
// Refresh preferences cache
preferencesStore.cache();

View File

@ -29,7 +29,7 @@ define('pgadmin.settings', ['sources/pgadmin'], function(pgAdmin) {
// We will force unload method to not to save current layout
// and reload the window
show: function() {
pgAdmin.Browser.docker.resetLayout();
pgAdmin.Browser.docker.default_workspace.resetLayout();
},
};

View File

@ -43,6 +43,7 @@ define('app', [
initializeModules(pgAdmin);
initializeModules(pgAdmin.Browser);
initializeModules(pgAdmin.Tools);
pgAdmin.Browser.docker = {};
// Add menus from back end.
pgAdmin.Browser.utils.addBackendMenus(pgAdmin.Browser);

View File

@ -13,7 +13,7 @@ import { PrimaryButton } from './components/Buttons';
import { PgMenu, PgMenuDivider, PgMenuItem, PgSubMenu } from './components/Menu';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import AccountCircleRoundedIcon from '@mui/icons-material/AccountCircleRounded';
import { usePgAdmin } from '../../static/js/BrowserComponent';
import { usePgAdmin } from '../../static/js/PgAdminProvider';
import { useForceUpdate } from './custom_hooks';

View File

@ -1,13 +1,22 @@
import React, {useEffect, useMemo, useState } from 'react';
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, {Fragment, useEffect, useMemo, useState } from 'react';
import AppMenuBar from './AppMenuBar';
import ObjectBreadcrumbs from './components/ObjectBreadcrumbs';
import Layout, { LayoutDocker, getDefaultGroup } from './helpers/Layout';
import Layout, { LAYOUT_EVENTS, LayoutDocker, getDefaultGroup } from './helpers/Layout';
import gettext from 'sources/gettext';
import ObjectExplorer from './tree/ObjectExplorer';
import Properties from '../../misc/properties/Properties';
import SQL from '../../misc/sql/static/js/SQL';
import Statistics from '../../misc/statistics/static/js/Statistics';
import { BROWSER_PANELS } from '../../browser/static/js/constants';
import { BROWSER_PANELS, WORKSPACES } from '../../browser/static/js/constants';
import Dependencies from '../../misc/dependencies/static/js/Dependencies';
import Dependents from '../../misc/dependents/static/js/Dependents';
import ModalProvider from './helpers/ModalProvider';
@ -21,6 +30,9 @@ import PropTypes from 'prop-types';
import Processes from '../../misc/bgprocess/static/js/Processes';
import { useBeforeUnload } from './custom_hooks';
import pgWindow from 'sources/window';
import WorkspaceToolbar from '../../misc/workspaces/static/js/WorkspaceToolbar';
import { useWorkspace, WorkspaceProvider } from '../../misc/workspaces/static/js/WorkspaceProvider';
import { PgAdminProvider, usePgAdmin } from './PgAdminProvider';
const objectExplorerGroup = {
@ -60,36 +72,81 @@ export const defaultTabsData = [
processesPanelData,
];
let defaultLayout = {
dockbox: {
mode: 'vertical',
children: [
{
mode: 'horizontal',
children: [
{
size: 20,
tabs: [
LayoutDocker.getPanel({
id: BROWSER_PANELS.OBJECT_EXPLORER, title: gettext('Object Explorer'),
content: <ObjectExplorer />, group: 'object-explorer'
}),
],
},
{
size: 80,
id: BROWSER_PANELS.MAIN,
group: 'playground',
tabs: defaultTabsData.map((t)=>LayoutDocker.getPanel(t)),
panelLock: {panelStyle: 'playground'},
}
]
},
]
},
};
function Layouts({browser}) {
const pgAdmin = usePgAdmin();
const {config, enabled, currentWorkspace} = useWorkspace();
return (
<div style={{display: 'flex', height: (browser != 'Electron' ? 'calc(100% - 30px)' : '100%')}}>
{enabled && <WorkspaceToolbar/> }
<Layout
getLayoutInstance={(obj)=>{
pgAdmin.Browser.docker.default_workspace = obj;
}}
defaultLayout={defaultLayout}
layoutId='Browser/Layout'
savedLayout={pgAdmin.Browser.utils.layout}
groups={{
'object-explorer': objectExplorerGroup,
'playground': mainPanelGroup,
}}
noContextGroups={['object-explorer']}
resetToTabPanel={BROWSER_PANELS.MAIN}
enableToolEvents
isLayoutVisible={!enabled || currentWorkspace == WORKSPACES.DEFAULT}
/>
{enabled && config.map((item)=>(
<Layout
key={item.docker}
getLayoutInstance={(obj)=>{
pgAdmin.Browser.docker[item.docker] = obj;
obj.eventBus.fireEvent(LAYOUT_EVENTS.INIT);
}}
defaultLayout={item.layout}
groups={{
'playground': {...getDefaultGroup()},
}}
resetToTabPanel={BROWSER_PANELS.MAIN}
isLayoutVisible={currentWorkspace == item.workspace}
/>
))}
</div>
);
}
Layouts.propTypes = {
browser: PropTypes.string,
};
export default function BrowserComponent({pgAdmin}) {
let defaultLayout = {
dockbox: {
mode: 'vertical',
children: [
{
mode: 'horizontal',
children: [
{
size: 20,
tabs: [
LayoutDocker.getPanel({
id: BROWSER_PANELS.OBJECT_EXPLORER, title: gettext('Object Explorer'),
content: <ObjectExplorer />, group: 'object-explorer'
}),
],
},
{
size: 80,
id: BROWSER_PANELS.MAIN,
group: 'playground',
tabs: defaultTabsData.map((t)=>LayoutDocker.getPanel(t)),
panelLock: {panelStyle: 'playground'},
}
]
},
]
},
};
const {isLoading, failed, getPreferencesForModule} = usePreferences();
let { name: browser } = useMemo(()=>getBrowser(), []);
const [uiReady, setUiReady] = useState(false);
@ -122,39 +179,19 @@ export default function BrowserComponent({pgAdmin}) {
}
return (
<PgAdminContext.Provider value={pgAdmin}>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} onReady={()=>setUiReady(true)}/>
{browser != 'Electron' && <AppMenuBar />}
<div style={{height: (browser != 'Electron' ? 'calc(100% - 30px)' : '100%')}}>
<Layout
getLayoutInstance={(obj)=>{
pgAdmin.Browser.docker = obj;
}}
defaultLayout={defaultLayout}
layoutId='Browser/Layout'
savedLayout={pgAdmin.Browser.utils.layout}
groups={{
'object-explorer': objectExplorerGroup,
'playground': mainPanelGroup,
}}
noContextGroups={['object-explorer']}
resetToTabPanel={BROWSER_PANELS.MAIN}
/>
</div>
</ModalProvider>
<ObjectBreadcrumbs pgAdmin={pgAdmin} />
</PgAdminContext.Provider>
<PgAdminProvider value={pgAdmin}>
<WorkspaceProvider>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} onReady={()=>setUiReady(true)}/>
{browser != 'Electron' && <AppMenuBar />}
<Layouts browser={browser} />
</ModalProvider>
<ObjectBreadcrumbs pgAdmin={pgAdmin} />
</WorkspaceProvider>
</PgAdminProvider>
);
}
BrowserComponent.propTypes = {
pgAdmin: PropTypes.object,
};
export const PgAdminContext = React.createContext();
export function usePgAdmin() {
const pgAdmin = React.useContext(PgAdminContext);
return pgAdmin;
}

View File

@ -17,7 +17,7 @@ import PropTypes from 'prop-types';
import { FormFooterMessage, InputCheckbox, InputText, MESSAGE_TYPE } from '../components/FormComponents';
import { ModalContent, ModalFooter } from '../../../static/js/components/ModalContent';
export default function ConnectServerContent({closeModal, data, onOK, setHeight}) {
export default function ConnectServerContent({closeModal, data, onOK, setHeight, hideSavePassword=false}) {
const containerRef = useRef();
const firstEleRef = useRef();
@ -73,7 +73,7 @@ export default function ConnectServerContent({closeModal, data, onOK, setHeight}
<InputText inputRef={firstEleRef} type="password" value={formData['tunnel_password']} controlProps={{maxLength:null, autoComplete:'new-password'}}
onChange={(e)=>onTextChange(e, 'tunnel_password')} onKeyDown={(e)=>onKeyDown(e)} />
</Box>
<Box marginTop='12px' marginBottom='12px'>
<Box marginTop='12px' marginBottom='12px' visibility={data.hide_save_tunnel_password ? 'hidden' : 'unset'}>
<InputCheckbox controlProps={{label: gettext('Save Password')}} value={formData['save_tunnel_password']}
onChange={(e)=>onTextChange(e.target.checked, 'save_tunnel_password')} disabled={!data.allow_save_tunnel_password} />
</Box>
@ -96,7 +96,7 @@ export default function ConnectServerContent({closeModal, data, onOK, setHeight}
}} type="password" value={formData['password']} controlProps={{maxLength:null}}
onChange={(e)=>onTextChange(e, 'password')} onKeyDown={(e)=>onKeyDown(e)}/>
</Box>
<Box marginTop='12px'>
<Box marginTop='12px' visibility={hideSavePassword ? 'hidden' : 'unset'}>
<InputCheckbox controlProps={{label: gettext('Save Password')}} value={formData['save_password']}
onChange={(e)=>onTextChange(e.target.checked, 'save_password')} disabled={!data.allow_save_password} />
</Box>
@ -133,5 +133,6 @@ ConnectServerContent.propTypes = {
closeModal: PropTypes.func,
data: PropTypes.object,
onOK: PropTypes.func,
setHeight: PropTypes.func
setHeight: PropTypes.func,
hideSavePassword: PropTypes.bool
};

View File

@ -188,8 +188,8 @@ export function showChangeServerPassword() {
isPgPassFileUsed = arguments[4];
const panelId = BROWSER_PANELS.SEARCH_OBJECTS;
const onClose = ()=>{pgAdmin.Browser.docker.close(panelId);};
pgAdmin.Browser.docker.openDialog({
const onClose = ()=>{pgAdmin.Browser.docker.default_workspace.close(panelId);};
pgAdmin.Browser.docker.default_workspace.openDialog({
id: panelId,
title: title,
content: (
@ -230,8 +230,8 @@ export function showChangeServerPassword() {
export function showChangeUserPassword(url) {
const panelId = BROWSER_PANELS.SEARCH_OBJECTS;
const onClose = ()=>{pgAdmin.Browser.docker.close(panelId);};
pgAdmin.Browser.docker.openDialog({
const onClose = ()=>{pgAdmin.Browser.docker.default_workspace.close(panelId);};
pgAdmin.Browser.docker.default_workspace.openDialog({
id: panelId,
title: gettext('Change pgAdmin User Password'),
content: (
@ -288,8 +288,8 @@ export function showNamedRestorePoint() {
itemNodeData = arguments[3];
const panelId = BROWSER_PANELS.SEARCH_OBJECTS;
const onClose = ()=>{pgAdmin.Browser.docker.close(panelId);};
pgAdmin.Browser.docker.openDialog({
const onClose = ()=>{pgAdmin.Browser.docker.default_workspace.close(panelId);};
pgAdmin.Browser.docker.default_workspace.openDialog({
id: panelId,
title: title,
content: (

View File

@ -0,0 +1,30 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import PropTypes from 'prop-types';
const PgAdminContext = React.createContext();
export function usePgAdmin() {
const pgAdmin = React.useContext(PgAdminContext);
return pgAdmin;
}
export function PgAdminProvider({children, value}) {
return <PgAdminContext.Provider value={value}>
{children}
</PgAdminContext.Provider>;
}
PgAdminProvider.propTypes = {
children: PropTypes.object,
value: PropTypes.any
};

View File

@ -24,7 +24,7 @@ import PropTypes from 'prop-types';
import { DndProvider } from 'react-dnd';
import {HTML5Backend} from 'react-dnd-html5-backend';
import { usePgAdmin } from 'sources/BrowserComponent';
import { usePgAdmin } from 'sources/PgAdminProvider';
import {
PgReactTable, PgReactTableBody, PgReactTableHeader,
PgReactTableRow,

View File

@ -15,18 +15,15 @@ import InfoIcon from '@mui/icons-material/InfoRounded';
import HelpIcon from '@mui/icons-material/HelpRounded';
import PublishIcon from '@mui/icons-material/Publish';
import SaveIcon from '@mui/icons-material/Save';
import SettingsBackupRestoreIcon from
'@mui/icons-material/SettingsBackupRestore';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import Box from '@mui/material/Box';
import _ from 'lodash';
import PropTypes from 'prop-types';
import { parseApiError } from 'sources/api_instance';
import { usePgAdmin } from 'sources/BrowserComponent';
import { usePgAdmin } from 'sources/PgAdminProvider';
import { useIsMounted } from 'sources/custom_hooks';
import {
DefaultButton, PgIconButton
} from 'sources/components/Buttons';
import { DefaultButton, PgIconButton } from 'sources/components/Buttons';
import CustomPropTypes from 'sources/custom_prop_types';
import gettext from 'sources/gettext';
@ -38,12 +35,14 @@ import { SchemaStateContext } from './SchemaState';
import { StyledBox } from './StyledComponents';
import { useSchemaState } from './hooks';
import { getForQueryParams } from './common';
import { QueryToolIcon } from '../components/ExternalIcon';
import TerminalRoundedIcon from '@mui/icons-material/TerminalRounded';
/* If its the dialog */
export default function SchemaDialogView({
getInitData, viewHelperProps, loadingText, schema={}, showFooter=true,
isTabView=true, checkDirtyOnEnableSave=false, ...props
isTabView=true, checkDirtyOnEnableSave=false, customCloseBtnName=gettext('Close'), ...props
}) {
// View helper properties
const onDataChange = props.onDataChange;
@ -168,6 +167,10 @@ export default function SchemaDialogView({
return <PublishIcon />;
} else if(props.customSaveBtnIconType == 'done') {
return <DoneIcon />;
} else if(props.customSaveBtnIconType == 'Query Tool') {
return <QueryToolIcon />;
} else if(props.customSaveBtnIconType == 'PSQL') {
return <TerminalRoundedIcon style={{width:'unset'}}/>;
}
return <SaveIcon />;
};
@ -208,10 +211,13 @@ export default function SchemaDialogView({
</Box>
}
<Box marginLeft='auto'>
<DefaultButton data-test='Close' onClick={props.onClose}
startIcon={<CloseIcon />} className='Dialog-buttonMargin'>
{ gettext('Close') }
</DefaultButton>
{
Boolean(customCloseBtnName) &&
<DefaultButton data-test='Close' onClick={props.onClose}
startIcon={<CloseIcon />} className='Dialog-buttonMargin'>
{ customCloseBtnName }
</DefaultButton>
}
<ResetButton
onClick={onResetClick}
icon={<SettingsBackupRestoreIcon />}
@ -260,4 +266,5 @@ SchemaDialogView.propTypes = {
formClassName: CustomPropTypes.className,
Notifier: PropTypes.object,
checkDirtyOnEnableSave: PropTypes.bool,
customCloseBtnName: PropTypes.string,
};

View File

@ -18,7 +18,7 @@ import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import PropTypes from 'prop-types';
import { usePgAdmin } from 'sources/BrowserComponent';
import { usePgAdmin } from 'sources/PgAdminProvider';
import gettext from 'sources/gettext';
import { PgIconButton, PgButtonGroup } from 'sources/components/Buttons';
import CustomPropTypes from 'sources/custom_prop_types';

View File

@ -11,6 +11,7 @@ export default function rcdockOverride(theme) {
return {
'.dock-layout': {
height: '100%',
width: '100%',
...theme.mixins.panelBorder.top,
'& .dock-ink-bar': {
height: '2px',

View File

@ -9,7 +9,7 @@
import React, { useEffect, useLayoutEffect, useRef } from 'react';
import ReactDOM from 'react-dom/client';
import { usePgAdmin } from './BrowserComponent';
import { usePgAdmin } from './PgAdminProvider';
import { BROWSER_PANELS } from '../../browser/static/js/constants';
import PropTypes from 'prop-types';
import LayoutIframeTab from './helpers/Layout/LayoutIframeTab';
@ -35,8 +35,7 @@ ToolForm.propTypes = {
params: PropTypes.object,
};
export default function ToolView() {
export default function ToolView({dockerObj}) {
const pgAdmin = usePgAdmin();
useEffect(()=>{
@ -54,7 +53,17 @@ export default function ToolView() {
window.open(toolUrl);
}
} else {
pgAdmin.Browser.docker.openTab({
// Handler here will return which layout instance the tool should go in
// case of workspace layout.
let handler = pgAdmin.Browser.getDockerHandler?.(panelId);
if(!handler) {
handler = {
docker: dockerObj,
focus: ()=>{},
};
}
handler.focus();
handler.docker.openTab({
id: panelId,
title: panelId,
content: (
@ -73,3 +82,6 @@ export default function ToolView() {
}, []);
return <></>;
}
ToolView.propTypes = {
dockerObj: PropTypes.object
};

View File

@ -13,7 +13,7 @@ import {getHelpUrl, getEPASHelpUrl} from 'pgadmin.help';
import SchemaView from 'sources/SchemaView';
import url_for from 'sources/url_for';
import ErrorBoundary from './helpers/ErrorBoundary';
import { usePgAdmin } from './BrowserComponent';
import { usePgAdmin } from './PgAdminProvider';
import { BROWSER_PANELS } from '../../browser/static/js/constants';
import { generateNodeUrl } from '../../browser/static/js/node_ajax';
import usePreferences from '../../preferences/static/js/store';

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { styled } from '@mui/material/styles';
import CheckboxTree from 'react-checkbox-tree';

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { PgMenu, PgMenuDivider, PgMenuItem, PgSubMenu } from './Menu';
import PropTypes from 'prop-types';

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { styled } from '@mui/material/styles';
import { Box } from '@mui/material';

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import QueryToolSvg from '../../img/fonticon/query_tool.svg?svgr';
import ViewDataSvg from '../../img/fonticon/view_data.svg?svgr';
@ -23,9 +32,9 @@ import ExecuteQuerySvg from '../../img/execute_query.svg?svgr';
import MagicSvg from '../../img/magic.svg?svgr';
import MsAzure from '../../img/ms_azure.svg?svgr';
import GoogleCloud from '../../img/google-cloud-1.svg?svgr';
import TerminalSvg from '../../img/fonticon/terminal.svg?svgr';
import RowFilterSvg from '../../img/fonticon/row_filter.svg?svgr';
import SvgIcon from '@mui/material/SvgIcon';
import SchemaDiffSvg from '../../img/fonticon/compare.svg?svgr';
export default function ExternalIcon({Icon, ...props}) {
return <SvgIcon component={Icon} inheritViewBox {...props}/>;
@ -77,9 +86,6 @@ ExpandDialogIcon.propTypes = {style: PropTypes.object};
export const MinimizeDialogIcon = ({style})=><ExternalIcon Icon={Collapse} style={{height: '1.4rem', ...style}} data-label="MinimizeDialogIcon" />;
MinimizeDialogIcon.propTypes = {style: PropTypes.object};
export const TerminalIcon = ({style})=><ExternalIcon Icon={TerminalSvg} style={{height: '1.5rem', transform: 'scale(0.95)', ...style}} data-label="TerminalIcon" />;
TerminalIcon.propTypes = {style: PropTypes.object};
export const RowFilterIcon = ({style})=><ExternalIcon Icon={RowFilterSvg} style={{height: '1rem', ...style}} data-label="RowFilterIcon" />;
RowFilterIcon.propTypes = {style: PropTypes.object};
@ -109,3 +115,6 @@ MagicIcon.propTypes = {style: PropTypes.object};
export const MSAzureIcon = ({style})=><ExternalIcon Icon={MsAzure} style={{height: '6rem', width: '7rem', ...style}} data-label="MSAzureIcon" />;
MSAzureIcon.propTypes = {style: PropTypes.object};
export const SchemaDiffIcon = ({style})=><ExternalIcon Icon={SchemaDiffSvg} style={{height: '2rem', ...style}} data-label="SchemaDiffIcon" />;
SchemaDiffIcon.propTypes = {style: PropTypes.object};

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { styled } from '@mui/material/styles';
import React from 'react';
import PropTypes from 'prop-types';

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useRef } from 'react';
import CheckIcon from '@mui/icons-material/Check';
import PropTypes from 'prop-types';

View File

@ -12,7 +12,7 @@ import React, { useState, useEffect } from 'react';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import CommentIcon from '@mui/icons-material/Comment';
import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded';
import { usePgAdmin } from '../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../static/js/PgAdminProvider';
import usePreferences from '../../../preferences/static/js/store';
const StyledBox = styled(Box)(({theme}) => ({

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import cn from 'classnames';
import * as React from 'react';
import { ClasslistComposite } from 'aspen-decorations';

View File

@ -1,3 +1,11 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import * as React from 'react';
import {
FileTree,
@ -228,6 +236,8 @@ export class FileTreeX extends React.Component<IFileTreeXProps> {
this.activeFileDec.removeTarget(this.activeFile);
this.activeFile = null;
}
this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'deselected', fileH);
};
private readonly setPseudoActiveFile = async (fileOrDirOrPath: FileOrDir | string): Promise<void> => {

View File

@ -1,10 +1,17 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { styled } from '@mui/material/styles';
import _ from 'lodash';
import React from 'react';
import { InputCheckbox, InputText } from './FormComponents';
import PropTypes from 'prop-types';
const Root = styled('div')(()=>({
/* Display the privs table only when focussed */
'&:not(:focus-within) .priv-table': {

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useState } from 'react';
import { Box} from '@mui/material';
import {InputSelect, FormInput} from './FormComponents';

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { styled } from '@mui/material/styles';
import PropTypes from 'prop-types';
@ -6,7 +15,6 @@ import _ from 'lodash';
import CustomPropTypes from '../custom_prop_types';
import gettext from 'sources/gettext';
const Root = styled('div')(({theme}) => ({
'& .ShortcutTitle-title': {
width: '100%',

View File

@ -1,3 +1,11 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
export default class EventBus {

View File

@ -61,8 +61,8 @@ export default function LayoutIframeTab({target, src, children}) {
if(r) setIframeTarget(r.querySelector('#'+target));
}} container={document.querySelector('#layout-portal')}>
{src ?
<iframe src={src} title=" " id={target} style={{position: 'fixed', border: 0}} />:
<Frame src={src} id={target} style={{position: 'fixed', border: 0}}>
<iframe src={src} title=" " id={target} style={{position: 'fixed', border: 0, zIndex: 1}} />:
<Frame src={src} id={target} style={{position: 'fixed', border: 0, zIndex: 1}}>
{children}
</Frame>
}

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useRef, useMemo, useEffect, useCallback, useState } from 'react';
import DockLayout from 'rc-dock';
import PropTypes from 'prop-types';
@ -374,7 +383,7 @@ export function getDefaultGroup() {
};
}
export default function Layout({groups, noContextGroups, getLayoutInstance, layoutId, savedLayout, resetToTabPanel, ...props}) {
export default function Layout({groups, noContextGroups, getLayoutInstance, layoutId, savedLayout, resetToTabPanel, enableToolEvents=false, isLayoutVisible=true, ...props}) {
const [[contextPos, contextPanelId, contextExtraMenus], setContextPos] = React.useState([null, null, null]);
const defaultGroups = React.useMemo(()=>({
'dialogs': getDialogsGroup(),
@ -458,34 +467,38 @@ export default function Layout({groups, noContextGroups, getLayoutInstance, layo
return (
<LayoutDockerContext.Provider value={layoutDockerObj}>
{useMemo(()=>(<DockLayout
style={{
height: '100%',
}}
ref={(obj)=>{
if(obj) {
layoutDockerObj.layoutObj = obj;
getLayoutInstance?.(layoutDockerObj);
layoutDockerObj.loadLayout(savedLayout);
}
}}
groups={defaultGroups}
onLayoutChange={(l, currentTabId, direction)=>{
if(Object.values(LAYOUT_EVENTS).indexOf(direction) > -1) {
layoutDockerObj.eventBus.fireEvent(LAYOUT_EVENTS[direction.toUpperCase()], currentTabId);
layoutDockerObj.saveLayout(l);
} else if(direction && direction != 'update') {
layoutDockerObj.eventBus.fireEvent(LAYOUT_EVENTS.CHANGE, currentTabId);
layoutDockerObj.saveLayout(l);
}
}}
{...props}
/>), [])}
<Box height="100%" width="100%" display={isLayoutVisible ? 'initial' : 'none'} >
{useMemo(()=>(<DockLayout
style={{
height: '100%',
}}
ref={(obj)=>{
if(obj) {
layoutDockerObj.layoutObj = obj;
getLayoutInstance?.(layoutDockerObj);
layoutDockerObj.loadLayout(savedLayout);
}
}}
groups={defaultGroups}
onLayoutChange={(l, currentTabId, direction)=>{
if(Object.values(LAYOUT_EVENTS).indexOf(direction) > -1) {
layoutDockerObj.eventBus.fireEvent(LAYOUT_EVENTS[direction.toUpperCase()], currentTabId);
layoutDockerObj.saveLayout(l);
} else if(direction && direction != 'update') {
layoutDockerObj.eventBus.fireEvent(LAYOUT_EVENTS.CHANGE, currentTabId);
layoutDockerObj.saveLayout(l);
}
}}
{...props}
/>), [])}
</Box>
<div id="layout-portal"></div>
<ContextMenu menuItems={contextMenuItems} position={contextPos} onClose={()=>setContextPos([null, null, null])}
label="Layout Context Menu" />
<UtilityView dockerObj={layoutDockerObj} />
<ToolView dockerObj={layoutDockerObj} />
{enableToolEvents && <>
<UtilityView dockerObj={layoutDockerObj} />
<ToolView dockerObj={layoutDockerObj} />
</>}
</LayoutDockerContext.Provider>
);
}
@ -498,10 +511,13 @@ Layout.propTypes = {
layoutId: PropTypes.string,
savedLayout: PropTypes.string,
resetToTabPanel: PropTypes.string,
enableToolEvents: PropTypes.bool,
isLayoutVisible: PropTypes.bool
};
export const LAYOUT_EVENTS = {
INIT: 'init',
ACTIVE: 'active',
REMOVE: 'remove',
FLOAT: 'float',

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { Box } from '@mui/material';
import { LAYOUT_EVENTS, LayoutDockerContext } from './Layout';

View File

@ -1,7 +1,17 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useState } from 'react';
import { usePgAdmin } from '../BrowserComponent';
import { usePgAdmin } from '../PgAdminProvider';
import { Box } from '@mui/material';
import { QueryToolIcon, RowFilterIcon, TerminalIcon, ViewDataIcon } from '../components/ExternalIcon';
import { QueryToolIcon, RowFilterIcon, ViewDataIcon } from '../components/ExternalIcon';
import TerminalRoundedIcon from '@mui/icons-material/TerminalRounded';
import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined';
import { PgButtonGroup, PgIconButton } from '../components/Buttons';
import _ from 'lodash';
@ -66,7 +76,7 @@ export default function ObjectExplorerToolbar() {
<ToolbarButton icon={<ViewDataIcon />} menuItem={menus['view_all_rows_context']} shortcut={browserPref?.sub_menu_view_data} />
<ToolbarButton icon={<RowFilterIcon />} menuItem={menus['view_filtered_rows_context']} />
<ToolbarButton icon={<SearchOutlinedIcon style={{height: '1.4rem'}} />} menuItem={menus['search_objects']} shortcut={browserPref?.sub_menu_search_objects} />
{!_.isUndefined(menus['psql']) && <ToolbarButton icon={<TerminalIcon />} menuItem={menus['psql']} />}
{!_.isUndefined(menus['psql']) && <ToolbarButton icon={<TerminalRoundedIcon style={{height: '1.4rem'}}/>} menuItem={menus['psql']} />}
</PgButtonGroup>
</Box>
);

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import pgAdmin from 'sources/pgadmin';
import 'pgadmin.tools.file_manager';

View File

@ -10,7 +10,7 @@
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { LayoutDockerContext, LAYOUT_EVENTS } from './Layout';
import { usePgAdmin } from '../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../static/js/PgAdminProvider';
import ErrorBoundary from './ErrorBoundary';
export default function withStandardTabInfo(Component, tabId) {

View File

@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {Tree} from './tree';
import * as pgadminUtils from '../utils';
@ -8,7 +17,7 @@ import { FileTreeX, TreeModelX } from '../components/PgTree';
import ContextMenu from '../components/ContextMenu';
import { generateNodeUrl } from '../../../browser/static/js/node_ajax';
import { copyToClipboard } from '../clipboard';
import { usePgAdmin } from '../BrowserComponent';
import { usePgAdmin } from '../PgAdminProvider';
function postTreeReady(b) {
const draggableTypes = [

View File

@ -15,6 +15,7 @@ import getApiInstance from './api_instance';
import usePreferences from '../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
import { isMac } from './keyboard_shortcuts';
import { WORKSPACES } from '../../browser/static/js/constants';
export function parseShortcutValue(obj) {
let shortcut = '';
@ -606,34 +607,6 @@ export function fullHexColor(shortHex) {
return shortHex;
}
export function gettextForTranslation(translations, ...replaceArgs) {
const text = replaceArgs[0];
let rawTranslation = translations[text] ? translations[text] : text;
if(arguments.length == 2) {
return rawTranslation;
}
try {
return rawTranslation.split('%s')
.map(function(w, i) {
if(i > 0) {
if(i < replaceArgs.length) {
return [replaceArgs[i], w].join('');
} else {
return ['%s', w].join('');
}
} else {
return w;
}
})
.join('');
} catch(e) {
console.error(e);
return rawTranslation;
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame
const requestAnimationFrame =
window.requestAnimationFrame ||
@ -760,6 +733,10 @@ export function getPlatform() {
}
}
export function isDefaultWorkspace() {
return pgAdmin.Browser?.docker?.currentWorkspace == WORKSPACES.DEFAULT;
}
/**
* Decimal adjustment of a number.
*

View File

@ -28,7 +28,7 @@ import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import usePreferences, { listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
import { PgAdminContext } from '../../../../static/js/BrowserComponent';
import { PgAdminProvider } from '../../../../static/js/PgAdminProvider';
export default class DebuggerModule {
static instance;
@ -573,12 +573,12 @@ export default class DebuggerModule {
const root = ReactDOM.createRoot(container);
root.render(
<Theme>
<PgAdminContext.Provider value={pgAdmin}>
<PgAdminProvider value={pgAdmin}>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} />
<DebuggerComponent pgAdmin={pgWindow.pgAdmin} selectedNodeInfo={selectedNodeInfo}
panelId={`${BROWSER_PANELS.DEBUGGER_TOOL}_${this.trans_id}`}
panelDocker={pgWindow.pgAdmin.Browser.docker}
panelDocker={pgWindow.pgAdmin.Browser.docker.default_workspace}
layout={layout} params={{
transId: trans_id,
directDebugger: this,
@ -586,7 +586,7 @@ export default class DebuggerModule {
}}
/>
</ModalProvider>
</PgAdminContext.Provider>
</PgAdminProvider>
</Theme>
);
}

View File

@ -19,7 +19,7 @@ import getApiInstance from '../../../../../static/js/api_instance';
import CodeMirror from '../../../../../static/js/components/ReactCodeMirror';
import { DEBUGGER_EVENTS } from '../DebuggerConstants';
import { DebuggerContext, DebuggerEventsContext } from './DebuggerComponent';
import { usePgAdmin } from '../../../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../../../static/js/PgAdminProvider';
import { isShortcutValue, parseKeyEventValue, parseShortcutValue } from '../../../../../static/js/utils';

View File

@ -20,7 +20,7 @@ import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import usePreferences, { listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
import { PgAdminContext } from '../../../../static/js/BrowserComponent';
import { PgAdminProvider } from '../../../../static/js/PgAdminProvider';
export function setPanelTitle(docker, panelId, panelTitle) {
docker.setTitle(panelId, panelTitle);
@ -147,7 +147,7 @@ export default class ERDModule {
const root = ReactDOM.createRoot(container);
root.render(
<Theme>
<PgAdminContext.Provider value={pgAdmin}>
<PgAdminProvider value={pgAdmin}>
<ModalProvider>
<NotifierProvider pgAdmin={this.pgAdmin} pgWindow={pgWindow} />
<ERDTool
@ -155,10 +155,10 @@ export default class ERDModule {
pgWindow={pgWindow}
pgAdmin={this.pgAdmin}
panelId={`${BROWSER_PANELS.ERD_TOOL}_${params.trans_id}`}
panelDocker={pgWindow.pgAdmin.Browser.docker}
panelDocker={pgWindow.pgAdmin.Browser.docker.default_workspace}
/>
</ModalProvider>
</PgAdminContext.Provider>
</PgAdminProvider>
</Theme>
);
}

View File

@ -21,7 +21,7 @@ import getApiInstance from '../../../../static/js/api_instance';
import SchemaView from '../../../../static/js/SchemaView';
import PropTypes from 'prop-types';
import PrivilegeSchema from './privilege_schema.ui';
import { usePgAdmin } from '../../../../static/js/BrowserComponent';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
export default function GrantWizard({ sid, did, nodeInfo, nodeData, onClose }) {

View File

@ -83,13 +83,13 @@ define([
const panelTitle = gettext('Grant Wizard');
const panelId = BROWSER_PANELS.GRANT_WIZARD;
pgBrowser.docker.openDialog({
pgBrowser.docker.default_workspace.openDialog({
id: panelId,
title: panelTitle,
manualClose: false,
content: (
<GrantWizard sid={sid} did={did} nodeInfo={info} nodeData={d}
onClose={()=>{pgBrowser.docker.close(panelId);}}
onClose={()=>{pgBrowser.docker.default_workspace.close(panelId);}}
/>
)
}, pgBrowser.stdW.lg, pgBrowser.stdH.lg);

View File

@ -89,7 +89,8 @@ def get_servers():
# Loop through all the servers for specific server group
servers = Server.query.filter(
Server.user_id == current_user.id,
Server.servergroup_id == group.id)
Server.servergroup_id == group.id,
Server.is_adhoc == 0)
for server in servers:
children.append({'value': server.id, 'label': server.name})

View File

@ -12,6 +12,7 @@ import gettext from 'sources/gettext';
import ImportExportServers from './ImportExportServers';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import pgAdmin from 'sources/pgadmin';
import { isDefaultWorkspace } from '../../../../static/js/utils';
export default class ImportExportServersModule {
static instance;
@ -34,7 +35,7 @@ export default class ImportExportServersModule {
module: this,
applies: ['tools'],
callback: 'showImportExportServers',
enable: true,
enable: isDefaultWorkspace,
priority: 3,
label: gettext('Import/Export Servers...'),
}];
@ -46,12 +47,12 @@ export default class ImportExportServersModule {
showImportExportServers() {
const panelTitle = gettext('Import/Export Servers');
const panelId = BROWSER_PANELS.IMPORT_EXPORT_SERVERS;
pgAdmin.Browser.docker.openDialog({
pgAdmin.Browser.docker.default_workspace.openDialog({
id: panelId,
title: panelTitle,
manualClose: false,
content: (
<ImportExportServers onClose={()=>{pgAdmin.Browser.docker.close(panelId);}}/>
<ImportExportServers onClose={()=>{pgAdmin.Browser.docker.default_workspace.close(panelId);}}/>
)
}, pgAdmin.Browser.stdW.lg, pgAdmin.Browser.stdH.lg);
}

View File

@ -308,6 +308,17 @@ def start_process(data):
_, manager = _get_connection(int(data['sid']), data)
psql_utility = manager.utility('sql')
if psql_utility is None:
sio.emit('pty-output',
{
'result': gettext(
'PSQL utility not found. Specify the binary '
'path in the preferences for the appropriate '
'server version, or select "Set as default" '
'to use an existing binary path.'),
'error': True},
namespace='/pty', room=request.sid)
return
connection_data = get_connection_str(psql_utility, db,
manager)
except Exception as e:

View File

@ -17,7 +17,7 @@ import pgWindow from 'sources/window';
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'pgadmin.browser';
import PsqlComponent from './components/PsqlComponent';
import { PgAdminContext } from '../../../../static/js/BrowserComponent';
import { PgAdminProvider } from '../../../../static/js/PgAdminProvider';
import getApiInstance from '../../../../static/js/api_instance';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
@ -187,12 +187,12 @@ export default class Psql {
const root = ReactDOM.createRoot(container);
root.render(
<Theme>
<PgAdminContext.Provider value={pgAdmin}>
<PgAdminProvider value={pgAdmin}>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} />
<PsqlComponent params={params} pgAdmin={pgAdmin} />
</ModalProvider>
</PgAdminContext.Provider>
</PgAdminProvider>
</Theme>
);
}

View File

@ -274,7 +274,8 @@ def servers():
server_icon_and_background
for server in Server.query.filter(
or_(Server.user_id == current_user.id, Server.shared)):
or_(Server.user_id == current_user.id, Server.shared),
Server.is_adhoc == 0):
shared_server = SharedServer.query.filter_by(
name=server.name, user_id=current_user.id,

Some files were not shown because too many files have changed in this diff Show More