Enhanced pgAdmin 4 with support for Workspace layouts. #7708
@ -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`.
|
||||
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 155 KiB |
BIN
docs/en_US/images/preferences_menu.png
Normal file
After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 148 KiB |
BIN
docs/en_US/images/preferences_misc_user_interface.png
Normal file
After Width: | Height: | Size: 209 KiB |
Before Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 238 KiB |
BIN
docs/en_US/images/psql_workspace.png
Normal file
After Width: | Height: | Size: 348 KiB |
BIN
docs/en_US/images/query_tool_workspace.png
Normal file
After Width: | Height: | Size: 361 KiB |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 124 KiB |
BIN
docs/en_US/images/schema_diff_workspace.png
Normal file
After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 98 KiB |
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
@ -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
|
||||
*******
|
||||
|
||||
|
@ -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
|
||||
|
31
docs/en_US/release_notes_9_0.rst
Normal 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
|
||||
*********
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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": "",
|
||||
|
39
web/migrations/versions/255e2842e4d7_.py
Normal 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
|
@ -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)
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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')]
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 = [
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: (
|
||||
|
@ -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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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%',
|
||||
|
@ -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%',
|
||||
|
@ -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}) => ({
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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}) => ({
|
||||
|
@ -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%',
|
||||
|
190
web/pgadmin/misc/workspaces/__init__.py
Normal 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)
|
1717
web/pgadmin/misc/workspaces/static/img/welcome_background.svg
Normal file
After Width: | Height: | Size: 433 KiB |
452
web/pgadmin/misc/workspaces/static/js/AdHocConnection.jsx
Normal 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
|
||||
};
|
113
web/pgadmin/misc/workspaces/static/js/WorkspaceProvider.jsx
Normal 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
|
||||
};
|
121
web/pgadmin/misc/workspaces/static/js/WorkspaceToolbar.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
130
web/pgadmin/misc/workspaces/static/js/WorkspaceWelcomePage.jsx
Normal 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
|
||||
};
|
96
web/pgadmin/misc/workspaces/static/js/config.jsx
Normal 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'},
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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: (
|
||||
|
30
web/pgadmin/static/js/PgAdminProvider.jsx
Normal 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
|
||||
};
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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};
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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}) => ({
|
||||
|
@ -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';
|
||||
|
@ -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> => {
|
||||
|
@ -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': {
|
||||
|
@ -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';
|
||||
|
@ -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%',
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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 = [
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 }) {
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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})
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|