diff --git a/docs/en_US/container_deployment.rst b/docs/en_US/container_deployment.rst index 4a9fa6ec196..c241d26aee4 100644 --- a/docs/en_US/container_deployment.rst +++ b/docs/en_US/container_deployment.rst @@ -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`. diff --git a/docs/en_US/images/main_left_pane.png b/docs/en_US/images/main_left_pane.png index c770412778c..96d6607dd85 100644 Binary files a/docs/en_US/images/main_left_pane.png and b/docs/en_US/images/main_left_pane.png differ diff --git a/docs/en_US/images/preferences_menu.png b/docs/en_US/images/preferences_menu.png new file mode 100644 index 00000000000..3aa9573ae8b Binary files /dev/null and b/docs/en_US/images/preferences_menu.png differ diff --git a/docs/en_US/images/preferences_misc_themes.png b/docs/en_US/images/preferences_misc_themes.png deleted file mode 100644 index 3b24296177f..00000000000 Binary files a/docs/en_US/images/preferences_misc_themes.png and /dev/null differ diff --git a/docs/en_US/images/preferences_misc_user_interface.png b/docs/en_US/images/preferences_misc_user_interface.png new file mode 100644 index 00000000000..ebe824fa9ac Binary files /dev/null and b/docs/en_US/images/preferences_misc_user_interface.png differ diff --git a/docs/en_US/images/preferences_misc_user_language.png b/docs/en_US/images/preferences_misc_user_language.png deleted file mode 100644 index 9020a280b39..00000000000 Binary files a/docs/en_US/images/preferences_misc_user_language.png and /dev/null differ diff --git a/docs/en_US/images/preferences_paths_binary.png b/docs/en_US/images/preferences_paths_binary.png index a76160284ef..1d07bf0942a 100644 Binary files a/docs/en_US/images/preferences_paths_binary.png and b/docs/en_US/images/preferences_paths_binary.png differ diff --git a/docs/en_US/images/psql_workspace.png b/docs/en_US/images/psql_workspace.png new file mode 100644 index 00000000000..98c4b66ab11 Binary files /dev/null and b/docs/en_US/images/psql_workspace.png differ diff --git a/docs/en_US/images/query_tool_workspace.png b/docs/en_US/images/query_tool_workspace.png new file mode 100644 index 00000000000..1a6d0e93a6a Binary files /dev/null and b/docs/en_US/images/query_tool_workspace.png differ diff --git a/docs/en_US/images/runtime_standalone.png b/docs/en_US/images/runtime_standalone.png index 63b27db314e..ef25a6dd811 100644 Binary files a/docs/en_US/images/runtime_standalone.png and b/docs/en_US/images/runtime_standalone.png differ diff --git a/docs/en_US/images/schema_diff_workspace.png b/docs/en_US/images/schema_diff_workspace.png new file mode 100644 index 00000000000..2b7a736166e Binary files /dev/null and b/docs/en_US/images/schema_diff_workspace.png differ diff --git a/docs/en_US/images/welcome.png b/docs/en_US/images/welcome.png index abd7bb6d957..e50828c65d3 100644 Binary files a/docs/en_US/images/welcome.png and b/docs/en_US/images/welcome.png differ diff --git a/docs/en_US/keyboard_shortcuts.rst b/docs/en_US/keyboard_shortcuts.rst index 984919bbb09..5f766637865 100644 --- a/docs/en_US/keyboard_shortcuts.rst +++ b/docs/en_US/keyboard_shortcuts.rst @@ -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 diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index 3e6e5949351..963f9232ea7 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -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 `_ 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 diff --git a/docs/en_US/psql_tool.rst b/docs/en_US/psql_tool.rst index 8a5046dcd4f..af539ac131b 100644 --- a/docs/en_US/psql_tool.rst +++ b/docs/en_US/psql_tool.rst @@ -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. \ No newline at end of file +: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 `_. +* 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. \ No newline at end of file diff --git a/docs/en_US/query_tool.rst b/docs/en_US/query_tool.rst index 88a0de9fb43..afe6d94e14c 100644 --- a/docs/en_US/query_tool.rst +++ b/docs/en_US/query_tool.rst @@ -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 `_. +* 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 ******* diff --git a/docs/en_US/release_notes.rst b/docs/en_US/release_notes.rst index f5640a1ebd0..24f800799a7 100644 --- a/docs/en_US/release_notes.rst +++ b/docs/en_US/release_notes.rst @@ -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 diff --git a/docs/en_US/release_notes_9_0.rst b/docs/en_US/release_notes_9_0.rst new file mode 100644 index 00000000000..2b45b76f6a8 --- /dev/null +++ b/docs/en_US/release_notes_9_0.rst @@ -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 `_ - Enhanced pgAdmin 4 with support for Workspace layouts. + +Housekeeping +************ + + +Bug fixes +********* + diff --git a/docs/en_US/schema_diff.rst b/docs/en_US/schema_diff.rst index 48b0e018889..331a3ef59fe 100644 --- a/docs/en_US/schema_diff.rst +++ b/docs/en_US/schema_diff.rst @@ -44,6 +44,18 @@ Use the :ref:`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. diff --git a/docs/en_US/user_interface.rst b/docs/en_US/user_interface.rst index 0670dace31b..41e25f143f9 100644 --- a/docs/en_US/user_interface.rst +++ b/docs/en_US/user_interface.rst @@ -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 ` to add a new server definition. + :ref:`Register - Server dialog ` to add a new server definition. * Click the *Configure pgAdmin* button to open the :ref:`Preferences dialog ` to customize your pgAdmin client. diff --git a/pkg/docker/entrypoint.sh b/pkg/docker/entrypoint.sh index aef32e77b61..c949670b544 100755 --- a/pkg/docker/entrypoint.sh +++ b/pkg/docker/entrypoint.sh @@ -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 diff --git a/web/config.py b/web/config.py index 1f034186729..fe16361ed8f 100644 --- a/web/config.py +++ b/web/config.py @@ -458,14 +458,12 @@ ########################################################################## 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 @@ 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": "", diff --git a/web/migrations/versions/255e2842e4d7_.py b/web/migrations/versions/255e2842e4d7_.py new file mode 100644 index 00000000000..336ab917554 --- /dev/null +++ b/web/migrations/versions/255e2842e4d7_.py @@ -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 diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 0b8790cc59e..fd2a7ed3bd8 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -401,78 +401,82 @@ def upgrade_db(): 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() - - # 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() - - # 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 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) + + 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() + + 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) diff --git a/web/pgadmin/about/static/js/AboutComponent.jsx b/web/pgadmin/about/static/js/AboutComponent.jsx index 6dd87ebf3ff..9f05c533698 100644 --- a/web/pgadmin/about/static/js/AboutComponent.jsx +++ b/web/pgadmin/about/static/js/AboutComponent.jsx @@ -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(); diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index 5d7ca75a740..72d3612e144 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -33,7 +33,8 @@ 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 @@ def get_nodes(self, gid): 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( - '', '.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 @@ def nodes(self, gid): 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 @@ def list(self, gid): 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 @@ def properties(self, gid, sid): 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 @@ def create(self, gid): ).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 @@ def create(self, gid): ) # 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 @@ def connect_status(self, gid, sid): } ) - 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 @@ def connect(self, gid, sid, is_qt=False): '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 @@ def connect(self, gid, sid, is_qt=False): 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 @@ def connect(self, gid, sid, is_qt=False): # 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 @@ def connect(self, gid, sid, is_qt=False): ) 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 @@ def connect(self, gid, sid, is_qt=False): 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 @@ def connect(self, gid, sid, is_qt=False): 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 @@ def get_response_for_password(self, server, status, prompt_password=False, ) else: data = { + "sid": server.id, "server_label": server.name, "username": server.username, "errmsg": errmsg, @@ -2073,7 +2012,7 @@ def get_response_for_password(self, server, status, prompt_password=False, "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, diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js index e90e0245d22..06cfc716b72 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js @@ -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')] - }]; - } } diff --git a/web/pgadmin/browser/server_groups/servers/utils.py b/web/pgadmin/browser/server_groups/servers/utils.py index 5c96baa9434..3f2115da6fe 100644 --- a/web/pgadmin/browser/server_groups/servers/utils.py +++ b/web/pgadmin/browser/server_groups/servers/utils.py @@ -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( + '', '.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 diff --git a/web/pgadmin/browser/static/js/constants.js b/web/pgadmin/browser/static/js/constants.js index d7bc7221a2f..c6ef09bf666 100644 --- a/web/pgadmin/browser/static/js/constants.js +++ b/web/pgadmin/browser/static/js/constants.js @@ -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 = [ diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js index 787fbd2c1b5..6e22bf41e77 100644 --- a/web/pgadmin/browser/static/js/keyboard.js +++ b/web/pgadmin/browser/static/js/keyboard.js @@ -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(); }); diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js index 2bd651eb41f..41ce7708956 100644 --- a/web/pgadmin/browser/static/js/node.js +++ b/web/pgadmin/browser/static/js/node.js @@ -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) { diff --git a/web/pgadmin/dashboard/static/js/Dashboard.jsx b/web/pgadmin/dashboard/static/js/Dashboard.jsx index 54c7c553e6f..9855cbb1202 100644 --- a/web/pgadmin/dashboard/static/js/Dashboard.jsx +++ b/web/pgadmin/dashboard/static/js/Dashboard.jsx @@ -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'; diff --git a/web/pgadmin/dashboard/static/js/Replication/LogReplication.jsx b/web/pgadmin/dashboard/static/js/Replication/LogReplication.jsx index 8dff0fc0620..99a9876a8b2 100644 --- a/web/pgadmin/dashboard/static/js/Replication/LogReplication.jsx +++ b/web/pgadmin/dashboard/static/js/Replication/LogReplication.jsx @@ -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'; diff --git a/web/pgadmin/dashboard/static/js/Replication/PGDReplication.jsx b/web/pgadmin/dashboard/static/js/Replication/PGDReplication.jsx index ea71e8b977a..8383407303a 100644 --- a/web/pgadmin/dashboard/static/js/Replication/PGDReplication.jsx +++ b/web/pgadmin/dashboard/static/js/Replication/PGDReplication.jsx @@ -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'; diff --git a/web/pgadmin/misc/__init__.py b/web/pgadmin/misc/__init__.py index 153f3191aab..71c6f69bb3f 100644 --- a/web/pgadmin/misc/__init__.py +++ b/web/pgadmin/misc/__init__.py @@ -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 @@ def register_preferences(self): # 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 @@ def register_preferences(self): }) 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 @@ def register_preferences(self): '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 @@ def register(self, app, options): from .statistics import blueprint as module self.submodules.append(module) + from .workspaces import blueprint as module + self.submodules.append(module) + super().register(app, options) diff --git a/web/pgadmin/misc/bgprocess/static/js/BgProcessManager.js b/web/pgadmin/misc/bgprocess/static/js/BgProcessManager.js index 7540c3f749c..a926d677496 100644 --- a/web/pgadmin/misc/bgprocess/static/js/BgProcessManager.js +++ b/web/pgadmin/misc/bgprocess/static/js/BgProcessManager.js @@ -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); } } diff --git a/web/pgadmin/misc/bgprocess/static/js/Processes.jsx b/web/pgadmin/misc/bgprocess/static/js/Processes.jsx index 0ab7b7a89fc..e3f800c66f4 100644 --- a/web/pgadmin/misc/bgprocess/static/js/Processes.jsx +++ b/web/pgadmin/misc/bgprocess/static/js/Processes.jsx @@ -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: ( diff --git a/web/pgadmin/misc/cloud/static/js/CloudWizard.jsx b/web/pgadmin/misc/cloud/static/js/CloudWizard.jsx index 567bccfb822..ba9dba5fa40 100644 --- a/web/pgadmin/misc/cloud/static/js/CloudWizard.jsx +++ b/web/pgadmin/misc/cloud/static/js/CloudWizard.jsx @@ -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); }; }, []); diff --git a/web/pgadmin/misc/cloud/static/js/cloud.js b/web/pgadmin/misc/cloud/static/js/cloud.js index 5ed62516ab9..e0910bc04d1 100644 --- a/web/pgadmin/misc/cloud/static/js/cloud.js +++ b/web/pgadmin/misc/cloud/static/js/cloud.js @@ -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); diff --git a/web/pgadmin/misc/dependencies/static/js/Dependencies.jsx b/web/pgadmin/misc/dependencies/static/js/Dependencies.jsx index 8f8b6707327..caf204818a5 100644 --- a/web/pgadmin/misc/dependencies/static/js/Dependencies.jsx +++ b/web/pgadmin/misc/dependencies/static/js/Dependencies.jsx @@ -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%', diff --git a/web/pgadmin/misc/dependents/static/js/Dependents.jsx b/web/pgadmin/misc/dependents/static/js/Dependents.jsx index 17d9822d04c..49b9da63c2a 100644 --- a/web/pgadmin/misc/dependents/static/js/Dependents.jsx +++ b/web/pgadmin/misc/dependents/static/js/Dependents.jsx @@ -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%', diff --git a/web/pgadmin/misc/properties/CollectionNodeProperties.jsx b/web/pgadmin/misc/properties/CollectionNodeProperties.jsx index 4781a70552f..862d36b206c 100644 --- a/web/pgadmin/misc/properties/CollectionNodeProperties.jsx +++ b/web/pgadmin/misc/properties/CollectionNodeProperties.jsx @@ -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}) => ({ diff --git a/web/pgadmin/misc/properties/ObjectNodeProperties.jsx b/web/pgadmin/misc/properties/ObjectNodeProperties.jsx index 7968a97b171..1623cbab184 100644 --- a/web/pgadmin/misc/properties/ObjectNodeProperties.jsx +++ b/web/pgadmin/misc/properties/ObjectNodeProperties.jsx @@ -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'; diff --git a/web/pgadmin/misc/properties/Properties.jsx b/web/pgadmin/misc/properties/Properties.jsx index 25cf7a7be6d..d6931edd296 100644 --- a/web/pgadmin/misc/properties/Properties.jsx +++ b/web/pgadmin/misc/properties/Properties.jsx @@ -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'; diff --git a/web/pgadmin/misc/sql/static/js/SQL.jsx b/web/pgadmin/misc/sql/static/js/SQL.jsx index 0448a41f471..8a3762f3d7f 100644 --- a/web/pgadmin/misc/sql/static/js/SQL.jsx +++ b/web/pgadmin/misc/sql/static/js/SQL.jsx @@ -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}) => ({ diff --git a/web/pgadmin/misc/statistics/static/js/Statistics.jsx b/web/pgadmin/misc/statistics/static/js/Statistics.jsx index 8e1356f605f..4bac898c85e 100644 --- a/web/pgadmin/misc/statistics/static/js/Statistics.jsx +++ b/web/pgadmin/misc/statistics/static/js/Statistics.jsx @@ -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%', diff --git a/web/pgadmin/misc/workspaces/__init__.py b/web/pgadmin/misc/workspaces/__init__.py new file mode 100644 index 00000000000..e148941b998 --- /dev/null +++ b/web/pgadmin/misc/workspaces/__init__.py @@ -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) diff --git a/web/pgadmin/misc/workspaces/static/img/welcome_background.svg b/web/pgadmin/misc/workspaces/static/img/welcome_background.svg new file mode 100644 index 00000000000..985b87659df --- /dev/null +++ b/web/pgadmin/misc/workspaces/static/img/welcome_background.svg @@ -0,0 +1,1717 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/misc/workspaces/static/js/AdHocConnection.jsx b/web/pgadmin/misc/workspaces/static/js/AdHocConnection.jsx new file mode 100644 index 00000000000..9cf9111f744 --- /dev/null +++ b/web/pgadmin/misc/workspaces/static/js/AdHocConnection.jsx @@ -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 ( + { + 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 ( + { + 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 { /*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 +}; diff --git a/web/pgadmin/misc/workspaces/static/js/WorkspaceProvider.jsx b/web/pgadmin/misc/workspaces/static/js/WorkspaceProvider.jsx new file mode 100644 index 00000000000..4f39ca7b48f --- /dev/null +++ b/web/pgadmin/misc/workspaces/static/js/WorkspaceProvider.jsx @@ -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 + {children} + ; +} + +WorkspaceProvider.propTypes = { + children: PropTypes.array +}; diff --git a/web/pgadmin/misc/workspaces/static/js/WorkspaceToolbar.jsx b/web/pgadmin/misc/workspaces/static/js/WorkspaceToolbar.jsx new file mode 100644 index 00000000000..6626f1ce17f --- /dev/null +++ b/web/pgadmin/misc/workspaces/static/js/WorkspaceToolbar.jsx @@ -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 ( + { + 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 ( + + } value={WORKSPACES.DEFAULT} /> + } value={WORKSPACES.QUERY_TOOL} /> + } value={WORKSPACES.PSQL_TOOL} /> + } value={WORKSPACES.SCHEMA_DIFF_TOOL} /> + + } menuItem={menus['settings']} /> + + + ); +} + diff --git a/web/pgadmin/misc/workspaces/static/js/WorkspaceWelcomePage.jsx b/web/pgadmin/misc/workspaces/static/js/WorkspaceWelcomePage.jsx new file mode 100644 index 00000000000..d37eb610486 --- /dev/null +++ b/web/pgadmin/misc/workspaces/static/js/WorkspaceWelcomePage.jsx @@ -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()); +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 = ; + 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 = ; + 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 ( + + + + +
{welcomeIcon}
+ + {welcomeTitle} + + + {welcomeFirst} + + + {welcomeSecond} + +
+ + + {gettext('Let\'s connect to the server')} + + + +
+
+ ); +} + +WorkspaceWelcomePage.propTypes = { + mode: PropTypes.string +}; diff --git a/web/pgadmin/misc/workspaces/static/js/config.jsx b/web/pgadmin/misc/workspaces/static/js/config.jsx new file mode 100644 index 00000000000..a10eeb1514f --- /dev/null +++ b/web/pgadmin/misc/workspaces/static/js/config.jsx @@ -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: , closable: false, group: 'playground' +}; + +const welcomePSQLPanelData = { + id: BROWSER_PANELS.WELCOME_PSQL_TOOL, title: gettext('Welcome'), content: , 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'}, + } + ] + }, + ] + } + } + }, +]; diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index 60adc1c09e6..9d8a14ffab5 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -33,7 +33,7 @@ # ########################################################################## -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): diff --git a/web/pgadmin/preferences/__init__.py b/web/pgadmin/preferences/__init__.py index bfd1125b713..053948d16ef 100644 --- a/web/pgadmin/preferences/__init__.py +++ b/web/pgadmin/preferences/__init__.py @@ -14,7 +14,7 @@ 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 diff --git a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx index d135576dbc5..2aeb3a81796 100644 --- a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx +++ b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx @@ -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.')}

+ ${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(); diff --git a/web/pgadmin/settings/static/js/settings.js b/web/pgadmin/settings/static/js/settings.js index 44d9a1fe7e9..e21339abe4b 100644 --- a/web/pgadmin/settings/static/js/settings.js +++ b/web/pgadmin/settings/static/js/settings.js @@ -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(); }, }; diff --git a/web/pgadmin/static/bundle/app.js b/web/pgadmin/static/bundle/app.js index f9b91c37160..55030e378b4 100644 --- a/web/pgadmin/static/bundle/app.js +++ b/web/pgadmin/static/bundle/app.js @@ -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); diff --git a/web/pgadmin/static/js/AppMenuBar.jsx b/web/pgadmin/static/js/AppMenuBar.jsx index 9f4164bf2ae..15d97ded054 100644 --- a/web/pgadmin/static/js/AppMenuBar.jsx +++ b/web/pgadmin/static/js/AppMenuBar.jsx @@ -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'; diff --git a/web/pgadmin/static/js/BrowserComponent.jsx b/web/pgadmin/static/js/BrowserComponent.jsx index 9ef4c2ce17c..8f7ffabf0ce 100644 --- a/web/pgadmin/static/js/BrowserComponent.jsx +++ b/web/pgadmin/static/js/BrowserComponent.jsx @@ -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: , 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 ( +
+ {enabled && } + { + 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)=>( + { + 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} + /> + ))} +
+ ); +} +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: , 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 ( - - - setUiReady(true)}/> - {browser != 'Electron' && } -
- { - 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} - /> -
-
- -
+ + + + setUiReady(true)}/> + {browser != 'Electron' && } + + + + + ); } BrowserComponent.propTypes = { pgAdmin: PropTypes.object, }; - -export const PgAdminContext = React.createContext(); - -export function usePgAdmin() { - const pgAdmin = React.useContext(PgAdminContext); - return pgAdmin; -} diff --git a/web/pgadmin/static/js/Dialogs/ConnectServerContent.jsx b/web/pgadmin/static/js/Dialogs/ConnectServerContent.jsx index 4d12a7c6f3d..011a77445ce 100644 --- a/web/pgadmin/static/js/Dialogs/ConnectServerContent.jsx +++ b/web/pgadmin/static/js/Dialogs/ConnectServerContent.jsx @@ -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} onTextChange(e, 'tunnel_password')} onKeyDown={(e)=>onKeyDown(e)} /> - + onTextChange(e.target.checked, 'save_tunnel_password')} disabled={!data.allow_save_tunnel_password} /> @@ -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)}/> - + onTextChange(e.target.checked, 'save_password')} disabled={!data.allow_save_password} /> @@ -133,5 +133,6 @@ ConnectServerContent.propTypes = { closeModal: PropTypes.func, data: PropTypes.object, onOK: PropTypes.func, - setHeight: PropTypes.func + setHeight: PropTypes.func, + hideSavePassword: PropTypes.bool }; diff --git a/web/pgadmin/static/js/Dialogs/index.jsx b/web/pgadmin/static/js/Dialogs/index.jsx index a3a12451dfa..71282e6bc86 100644 --- a/web/pgadmin/static/js/Dialogs/index.jsx +++ b/web/pgadmin/static/js/Dialogs/index.jsx @@ -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: ( diff --git a/web/pgadmin/static/js/PgAdminProvider.jsx b/web/pgadmin/static/js/PgAdminProvider.jsx new file mode 100644 index 00000000000..0c00b13f661 --- /dev/null +++ b/web/pgadmin/static/js/PgAdminProvider.jsx @@ -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 + {children} + ; +} + +PgAdminProvider.propTypes = { + children: PropTypes.object, + value: PropTypes.any +}; diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx index 179837fa469..03760d81768 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx @@ -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, diff --git a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx index 126fecd70d3..fd2f30ad73c 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx @@ -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 ; } else if(props.customSaveBtnIconType == 'done') { return ; + } else if(props.customSaveBtnIconType == 'Query Tool') { + return ; + } else if(props.customSaveBtnIconType == 'PSQL') { + return ; } return ; }; @@ -208,10 +211,13 @@ export default function SchemaDialogView({ } - } className='Dialog-buttonMargin'> - { gettext('Close') } - + { + Boolean(customCloseBtnName) && + } className='Dialog-buttonMargin'> + { customCloseBtnName } + + } } @@ -260,4 +266,5 @@ SchemaDialogView.propTypes = { formClassName: CustomPropTypes.className, Notifier: PropTypes.object, checkDirtyOnEnableSave: PropTypes.bool, + customCloseBtnName: PropTypes.string, }; diff --git a/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx b/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx index 23614c3753e..1394c038158 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx @@ -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'; diff --git a/web/pgadmin/static/js/Theme/overrides/rcdock.override.js b/web/pgadmin/static/js/Theme/overrides/rcdock.override.js index b56b52af282..8c59bee525b 100644 --- a/web/pgadmin/static/js/Theme/overrides/rcdock.override.js +++ b/web/pgadmin/static/js/Theme/overrides/rcdock.override.js @@ -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', diff --git a/web/pgadmin/static/js/ToolView.jsx b/web/pgadmin/static/js/ToolView.jsx index 2d9ad4692a4..f8e63cdce5e 100644 --- a/web/pgadmin/static/js/ToolView.jsx +++ b/web/pgadmin/static/js/ToolView.jsx @@ -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 +}; diff --git a/web/pgadmin/static/js/UtilityView.jsx b/web/pgadmin/static/js/UtilityView.jsx index 997693985c6..b131a008007 100644 --- a/web/pgadmin/static/js/UtilityView.jsx +++ b/web/pgadmin/static/js/UtilityView.jsx @@ -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'; diff --git a/web/pgadmin/static/js/components/CheckBoxTree.jsx b/web/pgadmin/static/js/components/CheckBoxTree.jsx index 436bf0403a9..8431e937cdf 100644 --- a/web/pgadmin/static/js/components/CheckBoxTree.jsx +++ b/web/pgadmin/static/js/components/CheckBoxTree.jsx @@ -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'; diff --git a/web/pgadmin/static/js/components/ContextMenu.jsx b/web/pgadmin/static/js/components/ContextMenu.jsx index 0479bb8e4e6..b3b15719b81 100644 --- a/web/pgadmin/static/js/components/ContextMenu.jsx +++ b/web/pgadmin/static/js/components/ContextMenu.jsx @@ -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'; diff --git a/web/pgadmin/static/js/components/EmptyPanelMessage.jsx b/web/pgadmin/static/js/components/EmptyPanelMessage.jsx index 1ace1fbff0f..90e0ac76992 100644 --- a/web/pgadmin/static/js/components/EmptyPanelMessage.jsx +++ b/web/pgadmin/static/js/components/EmptyPanelMessage.jsx @@ -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'; diff --git a/web/pgadmin/static/js/components/ExternalIcon.jsx b/web/pgadmin/static/js/components/ExternalIcon.jsx index 04eba6e8115..c36f0bffd66 100644 --- a/web/pgadmin/static/js/components/ExternalIcon.jsx +++ b/web/pgadmin/static/js/components/ExternalIcon.jsx @@ -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 ; @@ -77,9 +86,6 @@ ExpandDialogIcon.propTypes = {style: PropTypes.object}; export const MinimizeDialogIcon = ({style})=>; MinimizeDialogIcon.propTypes = {style: PropTypes.object}; -export const TerminalIcon = ({style})=>; -TerminalIcon.propTypes = {style: PropTypes.object}; - export const RowFilterIcon = ({style})=>; RowFilterIcon.propTypes = {style: PropTypes.object}; @@ -109,3 +115,6 @@ MagicIcon.propTypes = {style: PropTypes.object}; export const MSAzureIcon = ({style})=>; MSAzureIcon.propTypes = {style: PropTypes.object}; + +export const SchemaDiffIcon = ({style})=>; +SchemaDiffIcon.propTypes = {style: PropTypes.object}; \ No newline at end of file diff --git a/web/pgadmin/static/js/components/FieldSet.jsx b/web/pgadmin/static/js/components/FieldSet.jsx index d1c0f85024c..3b759c4b27e 100644 --- a/web/pgadmin/static/js/components/FieldSet.jsx +++ b/web/pgadmin/static/js/components/FieldSet.jsx @@ -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'; diff --git a/web/pgadmin/static/js/components/Menu.jsx b/web/pgadmin/static/js/components/Menu.jsx index 6acee6c5822..19079df2007 100644 --- a/web/pgadmin/static/js/components/Menu.jsx +++ b/web/pgadmin/static/js/components/Menu.jsx @@ -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'; diff --git a/web/pgadmin/static/js/components/ObjectBreadcrumbs.jsx b/web/pgadmin/static/js/components/ObjectBreadcrumbs.jsx index 05deecb860c..49f21a9fe1b 100644 --- a/web/pgadmin/static/js/components/ObjectBreadcrumbs.jsx +++ b/web/pgadmin/static/js/components/ObjectBreadcrumbs.jsx @@ -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}) => ({ diff --git a/web/pgadmin/static/js/components/PgTree/FileTreeItem/index.tsx b/web/pgadmin/static/js/components/PgTree/FileTreeItem/index.tsx index c3119d84b4b..c00bab9304b 100644 --- a/web/pgadmin/static/js/components/PgTree/FileTreeItem/index.tsx +++ b/web/pgadmin/static/js/components/PgTree/FileTreeItem/index.tsx @@ -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'; diff --git a/web/pgadmin/static/js/components/PgTree/FileTreeX/index.tsx b/web/pgadmin/static/js/components/PgTree/FileTreeX/index.tsx index d5be72e07de..24de3d588f7 100644 --- a/web/pgadmin/static/js/components/PgTree/FileTreeX/index.tsx +++ b/web/pgadmin/static/js/components/PgTree/FileTreeX/index.tsx @@ -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 { 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 => { diff --git a/web/pgadmin/static/js/components/Privilege.jsx b/web/pgadmin/static/js/components/Privilege.jsx index 43525b98141..a81675980a9 100644 --- a/web/pgadmin/static/js/components/Privilege.jsx +++ b/web/pgadmin/static/js/components/Privilege.jsx @@ -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': { diff --git a/web/pgadmin/static/js/components/SelectRefresh.jsx b/web/pgadmin/static/js/components/SelectRefresh.jsx index 3d463133e9a..f594340322a 100644 --- a/web/pgadmin/static/js/components/SelectRefresh.jsx +++ b/web/pgadmin/static/js/components/SelectRefresh.jsx @@ -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'; diff --git a/web/pgadmin/static/js/components/ShortcutTitle.jsx b/web/pgadmin/static/js/components/ShortcutTitle.jsx index f725da9e5e3..e6d33a75502 100644 --- a/web/pgadmin/static/js/components/ShortcutTitle.jsx +++ b/web/pgadmin/static/js/components/ShortcutTitle.jsx @@ -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%', diff --git a/web/pgadmin/static/js/helpers/EventBus.js b/web/pgadmin/static/js/helpers/EventBus.js index 47cef23af9f..a5743a0b091 100644 --- a/web/pgadmin/static/js/helpers/EventBus.js +++ b/web/pgadmin/static/js/helpers/EventBus.js @@ -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 { diff --git a/web/pgadmin/static/js/helpers/Layout/LayoutIframeTab.jsx b/web/pgadmin/static/js/helpers/Layout/LayoutIframeTab.jsx index 01244c7f68f..ea8193dafd5 100644 --- a/web/pgadmin/static/js/helpers/Layout/LayoutIframeTab.jsx +++ b/web/pgadmin/static/js/helpers/Layout/LayoutIframeTab.jsx @@ -61,8 +61,8 @@ export default function LayoutIframeTab({target, src, children}) { if(r) setIframeTarget(r.querySelector('#'+target)); }} container={document.querySelector('#layout-portal')}> {src ? -