Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for tags on a server node. #8192 #8223

Merged
merged 1 commit into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
adityatoshniwal marked this conversation as resolved.
Show resolved Hide resolved
* *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
Loading