Skip to content

Commit

Permalink
Added support for tags on a server node. #8192
Browse files Browse the repository at this point in the history
  • Loading branch information
adityatoshniwal committed Dec 5, 2024
1 parent 5e8a75c commit 99fe37c
Show file tree
Hide file tree
Showing 17 changed files with 218 additions and 37 deletions.
Binary file added docs/en_US/images/server_tags.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions docs/en_US/server_dialog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,18 @@ Use the fields in the *Advanced* tab to configure a connection:
.. toctree::

clear_saved_passwords


Click the *Tags* tab to continue.

.. image:: images/server_tags.png
:alt: Server dialog tags tab
:align: center

Use the table in the *Tags* tab to add tags. The tags will be shown on the right side of
a server node label in the object explorer tree.

Click on the *+* button to add a new tag. Some of the parameters are:

* *Text* field to specify the tag name.
* *Color* field to select the accent color of the tag.
35 changes: 35 additions & 0 deletions web/migrations/versions/f28be870d5ec_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2024, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

"""
Revision ID: f28be870d5ec
Revises: ac2c2e27dc2d
Create Date: 2024-11-29 14:59:30.882464
"""
from alembic import op, context
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'f28be870d5ec'
down_revision = 'ac2c2e27dc2d'
branch_labels = None
depends_on = None


def upgrade():
with op.batch_alter_table(
"server", table_kwargs={'sqlite_autoincrement': True}) as batch_op:
batch_op.add_column(sa.Column('tags', sa.JSON(), nullable=True))


def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass
72 changes: 64 additions & 8 deletions web/pgadmin/browser/server_groups/servers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
SERVER_CONNECTION_CLOSED
from sqlalchemy import or_
from sqlalchemy.orm.attributes import flag_modified
from pgadmin.utils.preferences import Preferences
from .... import socketio as sio
from pgadmin.utils import get_complete_file_path
Expand Down Expand Up @@ -278,7 +279,8 @@ def get_nodes(self, gid):
is_kerberos_conn=bool(server.kerberos_conn),
gss_authenticated=manager.gss_authenticated,
cloud_status=server.cloud_status,
description=server.comment
description=server.comment,
tags=server.tags
)

@property
Expand Down Expand Up @@ -550,6 +552,44 @@ def update_connection_parameter(self, data, server):

data['connection_params'] = existing_conn_params

@staticmethod
def update_tags(data, server):
"""
This function is used to update tags
"""
old_tags = getattr(server, 'tags', [])
# add old_text for comparison
old_tags = [{**tag, 'old_text': tag['text']}
for tag in old_tags] if old_tags is not None else []
new_tags_info = data.get('tags', None)

def update_tag(tags, changed):
for i, item in enumerate(tags):
if item['old_text'] == changed['old_text']:
item = {**item, **changed}
tags[i] = item
break

if new_tags_info:
deleted_ids = [t['old_text']
for t in new_tags_info.get('deleted', [])]
if len(deleted_ids) > 0:
old_tags = [
t for t in old_tags if t['old_text'] not in deleted_ids
]

for item in new_tags_info.get('changed', []):
update_tag(old_tags, item)

for item in new_tags_info.get('added', []):
old_tags.append(item)

# remove the old_text key
data['tags'] = [
{k: v for k, v in tag.items()
if k != 'old_text'} for tag in old_tags
]

@pga_login_required
def nodes(self, gid):
res = []
Expand Down Expand Up @@ -609,7 +649,8 @@ def nodes(self, gid):
shared=server.shared,
is_kerberos_conn=bool(server.kerberos_conn),
gss_authenticated=manager.gss_authenticated,
description=server.comment
description=server.comment,
tags=server.tags
)
)

Expand Down Expand Up @@ -678,7 +719,8 @@ def node(self, gid, sid):
shared=server.shared,
username=server.username,
is_kerberos_conn=bool(server.kerberos_conn),
gss_authenticated=manager.gss_authenticated
gss_authenticated=manager.gss_authenticated,
tags=server.tags
),
)

Expand Down Expand Up @@ -783,7 +825,8 @@ def update(self, gid, sid):
'shared_username': 'shared_username',
'kerberos_conn': 'kerberos_conn',
'connection_params': 'connection_params',
'prepare_threshold': 'prepare_threshold'
'prepare_threshold': 'prepare_threshold',
'tags': 'tags'
}

disp_lbl = {
Expand All @@ -808,6 +851,7 @@ def update(self, gid, sid):

# Update connection parameter if any.
self.update_connection_parameter(data, server)
self.update_tags(data, server)

if 'connection_params' in data and \
'hostaddr' in data['connection_params'] and \
Expand Down Expand Up @@ -838,6 +882,10 @@ def update(self, gid, sid):
errormsg=gettext('No parameters were changed.')
)

# tags is JSON type, sqlalchemy sometimes will not detect change
if 'tags' in data:
flag_modified(server, 'tags')

try:
db.session.commit()
except Exception as e:
Expand Down Expand Up @@ -872,7 +920,8 @@ def update(self, gid, sid):
username=server.username,
role=server.role,
is_password_saved=bool(server.save_password),
description=server.comment
description=server.comment,
tags=server.tags
)
)

Expand Down Expand Up @@ -1022,6 +1071,10 @@ def properties(self, gid, sid):
tunnel_authentication = bool(server.tunnel_authentication)
tunnel_keep_alive = server.tunnel_keep_alive

tags = None
if server.tags is not None:
tags = [{**tag, 'old_text': tag['text']}
for tag in server.tags]
response = {
'id': server.id,
'name': server.name,
Expand Down Expand Up @@ -1064,7 +1117,8 @@ def properties(self, gid, sid):
'cloud_status': server.cloud_status,
'connection_params': connection_params,
'connection_string': display_connection_str,
'prepare_threshold': server.prepare_threshold
'prepare_threshold': server.prepare_threshold,
'tags': tags,
}

return ajax_response(response)
Expand Down Expand Up @@ -1180,7 +1234,8 @@ def create(self, gid):
passexec_expiration=data.get('passexec_expiration', None),
kerberos_conn=1 if data.get('kerberos_conn', False) else 0,
connection_params=connection_params,
prepare_threshold=data.get('prepare_threshold', None)
prepare_threshold=data.get('prepare_threshold', None),
tags=data.get('tags', None)
)
db.session.add(server)
db.session.commit()
Expand Down Expand Up @@ -1273,7 +1328,8 @@ def create(self, gid):
manager and manager.gss_authenticated else False,
is_password_saved=bool(server.save_password),
is_tunnel_password_saved=tunnel_password_saved,
user_id=server.user_id
user_id=server.user_id,
tags=data.get('tags', None)
)
)

Expand Down
43 changes: 40 additions & 3 deletions web/pgadmin/browser/server_groups/servers/static/js/server.ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@ import {default as supportedServers} from 'pgadmin.server.supported_servers';
import current_user from 'pgadmin.user_management.current_user';
import { isEmptyString } from 'sources/validators';
import VariableSchema from './variable.ui';
import { getRandomColor } from '../../../../../static/js/utils';

class TagsSchema extends BaseUISchema {
get idAttribute() { return 'old_text'; }

get baseFields() {
return [
{
id: 'text', label: gettext('Text'), cell: 'text', group: null,
mode: ['create', 'edit'], noEmpty: true, controlProps: {
maxLength: 30,
}
},
{
id: 'color', label: gettext('Color'), cell: 'color', group: null,
mode: ['create', 'edit'], controlProps: {
input: true,
}
},
];
}

getNewData(data) {
return {
...data,
color: getRandomColor(),
};
}
}

export default class ServerSchema extends BaseUISchema {
constructor(serverGroupOptions=[], userId=0, initValues={}) {
Expand Down Expand Up @@ -50,11 +79,13 @@ export default class ServerSchema extends BaseUISchema {
connection_params: [
{'name': 'sslmode', 'value': 'prefer', 'keyword': 'sslmode'},
{'name': 'connect_timeout', 'value': 10, 'keyword': 'connect_timeout'}],
tags: [],
...initValues,
});

this.serverGroupOptions = serverGroupOptions;
this.paramSchema = new VariableSchema(this.getConnectionParameters(), null, null, ['name', 'keyword', 'value']);
this.tagsSchema = new TagsSchema();
this.userId = userId;
_.bindAll(this, 'isShared');
}
Expand Down Expand Up @@ -109,8 +140,8 @@ export default class ServerSchema extends BaseUISchema {
{
id: 'bgcolor', label: gettext('Background'), type: 'color',
group: null, mode: ['edit', 'create'],
disabled: obj.isConnected, deps: ['fgcolor'], depChange: (state)=>{
if(!state.bgcolor && state.fgcolor) {
disabled: obj.isConnected, deps: ['fgcolor'], depChange: (state, source)=>{
if(source[0] == 'fgcolor' && !state.bgcolor && state.fgcolor) {
return {'bgcolor': '#ffffff'};
}
}
Expand Down Expand Up @@ -365,7 +396,13 @@ export default class ServerSchema extends BaseUISchema {
mode: ['properties', 'edit', 'create'],
helpMessageMode: ['edit', 'create'],
helpMessage: gettext('If it is set to 0, every query is prepared the first time it is executed. If it is set to blank, prepared statements are disabled on the connection.')
}
},
{
id: 'tags', label: '',
type: 'collection', group: gettext('Tags'),
schema: this.tagsSchema, mode: ['edit', 'create'], uniqueCol: ['text'],
canAdd: true, canEdit: false, canDelete: true,
},
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,21 @@
"expected_data": {
"status_code": 200
}
},
{
"name": "Add server with tags",
"url": "/browser/server/obj/",
"is_positive_test": true,
"test_data": {
"tags": [
{"text": "tag1", "color": "#000"}
]
},
"mocking_required": false,
"mock_data": {},
"expected_data": {
"status_code": 200
}
}
],
"is_password_saved": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ def runTest(self):
if 'bgcolor' in self.test_data:
self.server['bgcolor'] = self.test_data['bgcolor']

if 'tags' in self.test_data:
self.server['tags'] = self.test_data['tags']

if self.is_positive_test:
if hasattr(self, 'with_save'):
self.server['save_password'] = self.with_save
Expand Down
3 changes: 2 additions & 1 deletion web/pgadmin/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
#
##########################################################################

SCHEMA_VERSION = 40
SCHEMA_VERSION = 41

##########################################################################
#
Expand Down Expand Up @@ -209,6 +209,7 @@ class Server(db.Model):
cloud_status = db.Column(db.Integer(), nullable=False, default=0)
connection_params = db.Column(MutableDict.as_mutable(types.JSON))
prepare_threshold = db.Column(db.Integer(), nullable=True)
tags = db.Column(types.JSON)


class ModulePreference(db.Model):
Expand Down
3 changes: 3 additions & 0 deletions web/pgadmin/static/js/SchemaView/MappedControl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
useFieldOptions, useFieldValue, useFieldError, useSchemaStateSubscriber,
} from './hooks';
import { listenDepChanges } from './utils';
import { InputColor } from '../components/FormComponents';


/* Control mapping for form view */
Expand Down Expand Up @@ -263,6 +264,8 @@ function MappedCellControlBase({
return <InputDateTimePicker name={name} value={value} onChange={onTextChange} {...props}/>;
case 'sql':
return <InputSQL name={name} value={value} onChange={onSqlChange} {...props} />;
case 'color':
return <InputColor name={name} value={value} onChange={onTextChange} {...props} />;
case 'file':
return <InputFileSelect name={name} value={value} onChange={onTextChange} inputRef={props.inputRef} {...props} />;
case 'keyCode':
Expand Down
Loading

0 comments on commit 99fe37c

Please sign in to comment.