Skip to content

Commit bd2a484

Browse files
Added support for adding tags on a server node. #8192
1 parent 5e8a75c commit bd2a484

File tree

17 files changed

+218
-37
lines changed

17 files changed

+218
-37
lines changed

docs/en_US/images/server_tags.png

42.1 KB
Loading

docs/en_US/server_dialog.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,18 @@ Use the fields in the *Advanced* tab to configure a connection:
233233
.. toctree::
234234

235235
clear_saved_passwords
236+
237+
238+
Click the *Tags* tab to continue.
239+
240+
.. image:: images/server_tags.png
241+
:alt: Server dialog tags tab
242+
:align: center
243+
244+
Use the table in the *Tags* tab to add tags. The tags will be shown on the right side of
245+
a server node label in the object explorer tree.
246+
247+
Click on the *+* button to add a new tag. Some of the parameters are:
248+
249+
* *Text* field to specify the tag name.
250+
* *Color* field to select the accent color of the tag.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
##########################################################################
2+
#
3+
# pgAdmin 4 - PostgreSQL Tools
4+
#
5+
# Copyright (C) 2013 - 2024, The pgAdmin Development Team
6+
# This software is released under the PostgreSQL Licence
7+
#
8+
##########################################################################
9+
10+
"""
11+
Revision ID: f28be870d5ec
12+
Revises: ac2c2e27dc2d
13+
Create Date: 2024-11-29 14:59:30.882464
14+
15+
"""
16+
from alembic import op, context
17+
import sqlalchemy as sa
18+
19+
20+
# revision identifiers, used by Alembic.
21+
revision = 'f28be870d5ec'
22+
down_revision = 'ac2c2e27dc2d'
23+
branch_labels = None
24+
depends_on = None
25+
26+
27+
def upgrade():
28+
with op.batch_alter_table(
29+
"server", table_kwargs={'sqlite_autoincrement': True}) as batch_op:
30+
batch_op.add_column(sa.Column('tags', sa.JSON(), nullable=True))
31+
32+
33+
def downgrade():
34+
# pgAdmin only upgrades, downgrade not implemented.
35+
pass

web/pgadmin/browser/server_groups/servers/__init__.py

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
3838
SERVER_CONNECTION_CLOSED
3939
from sqlalchemy import or_
40+
from sqlalchemy.orm.attributes import flag_modified
4041
from pgadmin.utils.preferences import Preferences
4142
from .... import socketio as sio
4243
from pgadmin.utils import get_complete_file_path
@@ -278,7 +279,8 @@ def get_nodes(self, gid):
278279
is_kerberos_conn=bool(server.kerberos_conn),
279280
gss_authenticated=manager.gss_authenticated,
280281
cloud_status=server.cloud_status,
281-
description=server.comment
282+
description=server.comment,
283+
tags=server.tags
282284
)
283285

284286
@property
@@ -550,6 +552,44 @@ def update_connection_parameter(self, data, server):
550552

551553
data['connection_params'] = existing_conn_params
552554

555+
@staticmethod
556+
def update_tags(data, server):
557+
"""
558+
This function is used to update tags
559+
"""
560+
old_tags = getattr(server, 'tags', [])
561+
# add old_text for comparison
562+
old_tags = [{**tag, 'old_text': tag['text']}
563+
for tag in old_tags] if old_tags is not None else []
564+
new_tags_info = data.get('tags', None)
565+
566+
def update_tag(tags, changed):
567+
for i, item in enumerate(tags):
568+
if item['old_text'] == changed['old_text']:
569+
item = {**item, **changed}
570+
tags[i] = item
571+
break
572+
573+
if new_tags_info:
574+
deleted_ids = [t['old_text']
575+
for t in new_tags_info.get('deleted', [])]
576+
if len(deleted_ids) > 0:
577+
old_tags = [
578+
t for t in old_tags if t['old_text'] not in deleted_ids
579+
]
580+
581+
for item in new_tags_info.get('changed', []):
582+
update_tag(old_tags, item)
583+
584+
for item in new_tags_info.get('added', []):
585+
old_tags.append(item)
586+
587+
# remove the old_text key
588+
data['tags'] = [
589+
{k: v for k, v in tag.items()
590+
if k != 'old_text'} for tag in old_tags
591+
]
592+
553593
@pga_login_required
554594
def nodes(self, gid):
555595
res = []
@@ -609,7 +649,8 @@ def nodes(self, gid):
609649
shared=server.shared,
610650
is_kerberos_conn=bool(server.kerberos_conn),
611651
gss_authenticated=manager.gss_authenticated,
612-
description=server.comment
652+
description=server.comment,
653+
tags=server.tags
613654
)
614655
)
615656

@@ -678,7 +719,8 @@ def node(self, gid, sid):
678719
shared=server.shared,
679720
username=server.username,
680721
is_kerberos_conn=bool(server.kerberos_conn),
681-
gss_authenticated=manager.gss_authenticated
722+
gss_authenticated=manager.gss_authenticated,
723+
tags=server.tags
682724
),
683725
)
684726

@@ -783,7 +825,8 @@ def update(self, gid, sid):
783825
'shared_username': 'shared_username',
784826
'kerberos_conn': 'kerberos_conn',
785827
'connection_params': 'connection_params',
786-
'prepare_threshold': 'prepare_threshold'
828+
'prepare_threshold': 'prepare_threshold',
829+
'tags': 'tags'
787830
}
788831

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

809852
# Update connection parameter if any.
810853
self.update_connection_parameter(data, server)
854+
self.update_tags(data, server)
811855

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

885+
# tags is JSON type, sqlalchemy sometimes will not detect change
886+
if 'tags' in data:
887+
flag_modified(server, 'tags')
888+
841889
try:
842890
db.session.commit()
843891
except Exception as e:
@@ -872,7 +920,8 @@ def update(self, gid, sid):
872920
username=server.username,
873921
role=server.role,
874922
is_password_saved=bool(server.save_password),
875-
description=server.comment
923+
description=server.comment,
924+
tags=server.tags
876925
)
877926
)
878927

@@ -1022,6 +1071,10 @@ def properties(self, gid, sid):
10221071
tunnel_authentication = bool(server.tunnel_authentication)
10231072
tunnel_keep_alive = server.tunnel_keep_alive
10241073

1074+
tags = None
1075+
if server.tags is not None:
1076+
tags = [{**tag, 'old_text': tag['text']}
1077+
for tag in server.tags]
10251078
response = {
10261079
'id': server.id,
10271080
'name': server.name,
@@ -1064,7 +1117,8 @@ def properties(self, gid, sid):
10641117
'cloud_status': server.cloud_status,
10651118
'connection_params': connection_params,
10661119
'connection_string': display_connection_str,
1067-
'prepare_threshold': server.prepare_threshold
1120+
'prepare_threshold': server.prepare_threshold,
1121+
'tags': tags,
10681122
}
10691123

10701124
return ajax_response(response)
@@ -1180,7 +1234,8 @@ def create(self, gid):
11801234
passexec_expiration=data.get('passexec_expiration', None),
11811235
kerberos_conn=1 if data.get('kerberos_conn', False) else 0,
11821236
connection_params=connection_params,
1183-
prepare_threshold=data.get('prepare_threshold', None)
1237+
prepare_threshold=data.get('prepare_threshold', None),
1238+
tags=data.get('tags', None)
11841239
)
11851240
db.session.add(server)
11861241
db.session.commit()
@@ -1273,7 +1328,8 @@ def create(self, gid):
12731328
manager and manager.gss_authenticated else False,
12741329
is_password_saved=bool(server.save_password),
12751330
is_tunnel_password_saved=tunnel_password_saved,
1276-
user_id=server.user_id
1331+
user_id=server.user_id,
1332+
tags=data.get('tags', None)
12771333
)
12781334
)
12791335

web/pgadmin/browser/server_groups/servers/static/js/server.ui.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,35 @@ import {default as supportedServers} from 'pgadmin.server.supported_servers';
1515
import current_user from 'pgadmin.user_management.current_user';
1616
import { isEmptyString } from 'sources/validators';
1717
import VariableSchema from './variable.ui';
18+
import { getRandomColor } from '../../../../../static/js/utils';
19+
20+
class TagsSchema extends BaseUISchema {
21+
get idAttribute() { return 'old_text'; }
22+
23+
get baseFields() {
24+
return [
25+
{
26+
id: 'text', label: gettext('Text'), cell: 'text', group: null,
27+
mode: ['create', 'edit'], noEmpty: true, controlProps: {
28+
maxLength: 30,
29+
}
30+
},
31+
{
32+
id: 'color', label: gettext('Color'), cell: 'color', group: null,
33+
mode: ['create', 'edit'], controlProps: {
34+
input: true,
35+
}
36+
},
37+
];
38+
}
39+
40+
getNewData(data) {
41+
return {
42+
...data,
43+
color: getRandomColor(),
44+
};
45+
}
46+
}
1847

1948
export default class ServerSchema extends BaseUISchema {
2049
constructor(serverGroupOptions=[], userId=0, initValues={}) {
@@ -50,11 +79,13 @@ export default class ServerSchema extends BaseUISchema {
5079
connection_params: [
5180
{'name': 'sslmode', 'value': 'prefer', 'keyword': 'sslmode'},
5281
{'name': 'connect_timeout', 'value': 10, 'keyword': 'connect_timeout'}],
82+
tags: [],
5383
...initValues,
5484
});
5585

5686
this.serverGroupOptions = serverGroupOptions;
5787
this.paramSchema = new VariableSchema(this.getConnectionParameters(), null, null, ['name', 'keyword', 'value']);
88+
this.tagsSchema = new TagsSchema();
5889
this.userId = userId;
5990
_.bindAll(this, 'isShared');
6091
}
@@ -109,8 +140,8 @@ export default class ServerSchema extends BaseUISchema {
109140
{
110141
id: 'bgcolor', label: gettext('Background'), type: 'color',
111142
group: null, mode: ['edit', 'create'],
112-
disabled: obj.isConnected, deps: ['fgcolor'], depChange: (state)=>{
113-
if(!state.bgcolor && state.fgcolor) {
143+
disabled: obj.isConnected, deps: ['fgcolor'], depChange: (state, source)=>{
144+
if(source[0] == 'fgcolor' && !state.bgcolor && state.fgcolor) {
114145
return {'bgcolor': '#ffffff'};
115146
}
116147
}
@@ -365,7 +396,13 @@ export default class ServerSchema extends BaseUISchema {
365396
mode: ['properties', 'edit', 'create'],
366397
helpMessageMode: ['edit', 'create'],
367398
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.')
368-
}
399+
},
400+
{
401+
id: 'tags', label: '',
402+
type: 'collection', group: gettext('Tags'),
403+
schema: this.tagsSchema, mode: ['edit', 'create'], uniqueCol: ['text'],
404+
canAdd: true, canEdit: false, canDelete: true,
405+
},
369406
];
370407
}
371408

web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,21 @@
225225
"expected_data": {
226226
"status_code": 200
227227
}
228+
},
229+
{
230+
"name": "Add server with tags",
231+
"url": "/browser/server/obj/",
232+
"is_positive_test": true,
233+
"test_data": {
234+
"tags": [
235+
{"text": "tag1", "color": "#000"}
236+
]
237+
},
238+
"mocking_required": false,
239+
"mock_data": {},
240+
"expected_data": {
241+
"status_code": 200
242+
}
228243
}
229244
],
230245
"is_password_saved": [

web/pgadmin/browser/server_groups/servers/tests/test_add_server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ def runTest(self):
8888
if 'bgcolor' in self.test_data:
8989
self.server['bgcolor'] = self.test_data['bgcolor']
9090

91+
if 'tags' in self.test_data:
92+
self.server['tags'] = self.test_data['tags']
93+
9194
if self.is_positive_test:
9295
if hasattr(self, 'with_save'):
9396
self.server['save_password'] = self.with_save

web/pgadmin/model/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
#
3434
##########################################################################
3535

36-
SCHEMA_VERSION = 40
36+
SCHEMA_VERSION = 41
3737

3838
##########################################################################
3939
#
@@ -209,6 +209,7 @@ class Server(db.Model):
209209
cloud_status = db.Column(db.Integer(), nullable=False, default=0)
210210
connection_params = db.Column(MutableDict.as_mutable(types.JSON))
211211
prepare_threshold = db.Column(db.Integer(), nullable=True)
212+
tags = db.Column(types.JSON)
212213

213214

214215
class ModulePreference(db.Model):

web/pgadmin/static/js/SchemaView/MappedControl.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
useFieldOptions, useFieldValue, useFieldError, useSchemaStateSubscriber,
3232
} from './hooks';
3333
import { listenDepChanges } from './utils';
34+
import { InputColor } from '../components/FormComponents';
3435

3536

3637
/* Control mapping for form view */
@@ -263,6 +264,8 @@ function MappedCellControlBase({
263264
return <InputDateTimePicker name={name} value={value} onChange={onTextChange} {...props}/>;
264265
case 'sql':
265266
return <InputSQL name={name} value={value} onChange={onSqlChange} {...props} />;
267+
case 'color':
268+
return <InputColor name={name} value={value} onChange={onTextChange} {...props} />;
266269
case 'file':
267270
return <InputFileSelect name={name} value={value} onChange={onTextChange} inputRef={props.inputRef} {...props} />;
268271
case 'keyCode':

0 commit comments

Comments
 (0)