mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-12-22 15:13:42 -06:00
Added support for adding tags on a server node. #8192
This commit is contained in:
parent
5e8a75cdf9
commit
bd2a484c2f
BIN
docs/en_US/images/server_tags.png
Normal file
BIN
docs/en_US/images/server_tags.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
@ -233,3 +233,18 @@ Use the fields in the *Advanced* tab to configure a connection:
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
||||||
clear_saved_passwords
|
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
web/migrations/versions/f28be870d5ec_.py
Normal file
35
web/migrations/versions/f28be870d5ec_.py
Normal file
@ -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
|
@ -37,6 +37,7 @@ from pgadmin.browser.server_groups.servers.utils import \
|
|||||||
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
|
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
|
||||||
SERVER_CONNECTION_CLOSED
|
SERVER_CONNECTION_CLOSED
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from pgadmin.utils.preferences import Preferences
|
from pgadmin.utils.preferences import Preferences
|
||||||
from .... import socketio as sio
|
from .... import socketio as sio
|
||||||
from pgadmin.utils import get_complete_file_path
|
from pgadmin.utils import get_complete_file_path
|
||||||
@ -278,7 +279,8 @@ class ServerModule(sg.ServerGroupPluginModule):
|
|||||||
is_kerberos_conn=bool(server.kerberos_conn),
|
is_kerberos_conn=bool(server.kerberos_conn),
|
||||||
gss_authenticated=manager.gss_authenticated,
|
gss_authenticated=manager.gss_authenticated,
|
||||||
cloud_status=server.cloud_status,
|
cloud_status=server.cloud_status,
|
||||||
description=server.comment
|
description=server.comment,
|
||||||
|
tags=server.tags
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -550,6 +552,44 @@ class ServerNode(PGChildNodeView):
|
|||||||
|
|
||||||
data['connection_params'] = existing_conn_params
|
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
|
@pga_login_required
|
||||||
def nodes(self, gid):
|
def nodes(self, gid):
|
||||||
res = []
|
res = []
|
||||||
@ -609,7 +649,8 @@ class ServerNode(PGChildNodeView):
|
|||||||
shared=server.shared,
|
shared=server.shared,
|
||||||
is_kerberos_conn=bool(server.kerberos_conn),
|
is_kerberos_conn=bool(server.kerberos_conn),
|
||||||
gss_authenticated=manager.gss_authenticated,
|
gss_authenticated=manager.gss_authenticated,
|
||||||
description=server.comment
|
description=server.comment,
|
||||||
|
tags=server.tags
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -678,7 +719,8 @@ class ServerNode(PGChildNodeView):
|
|||||||
shared=server.shared,
|
shared=server.shared,
|
||||||
username=server.username,
|
username=server.username,
|
||||||
is_kerberos_conn=bool(server.kerberos_conn),
|
is_kerberos_conn=bool(server.kerberos_conn),
|
||||||
gss_authenticated=manager.gss_authenticated
|
gss_authenticated=manager.gss_authenticated,
|
||||||
|
tags=server.tags
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -783,7 +825,8 @@ class ServerNode(PGChildNodeView):
|
|||||||
'shared_username': 'shared_username',
|
'shared_username': 'shared_username',
|
||||||
'kerberos_conn': 'kerberos_conn',
|
'kerberos_conn': 'kerberos_conn',
|
||||||
'connection_params': 'connection_params',
|
'connection_params': 'connection_params',
|
||||||
'prepare_threshold': 'prepare_threshold'
|
'prepare_threshold': 'prepare_threshold',
|
||||||
|
'tags': 'tags'
|
||||||
}
|
}
|
||||||
|
|
||||||
disp_lbl = {
|
disp_lbl = {
|
||||||
@ -808,6 +851,7 @@ class ServerNode(PGChildNodeView):
|
|||||||
|
|
||||||
# Update connection parameter if any.
|
# Update connection parameter if any.
|
||||||
self.update_connection_parameter(data, server)
|
self.update_connection_parameter(data, server)
|
||||||
|
self.update_tags(data, server)
|
||||||
|
|
||||||
if 'connection_params' in data and \
|
if 'connection_params' in data and \
|
||||||
'hostaddr' in data['connection_params'] and \
|
'hostaddr' in data['connection_params'] and \
|
||||||
@ -838,6 +882,10 @@ class ServerNode(PGChildNodeView):
|
|||||||
errormsg=gettext('No parameters were changed.')
|
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:
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -872,7 +920,8 @@ class ServerNode(PGChildNodeView):
|
|||||||
username=server.username,
|
username=server.username,
|
||||||
role=server.role,
|
role=server.role,
|
||||||
is_password_saved=bool(server.save_password),
|
is_password_saved=bool(server.save_password),
|
||||||
description=server.comment
|
description=server.comment,
|
||||||
|
tags=server.tags
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1022,6 +1071,10 @@ class ServerNode(PGChildNodeView):
|
|||||||
tunnel_authentication = bool(server.tunnel_authentication)
|
tunnel_authentication = bool(server.tunnel_authentication)
|
||||||
tunnel_keep_alive = server.tunnel_keep_alive
|
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 = {
|
response = {
|
||||||
'id': server.id,
|
'id': server.id,
|
||||||
'name': server.name,
|
'name': server.name,
|
||||||
@ -1064,7 +1117,8 @@ class ServerNode(PGChildNodeView):
|
|||||||
'cloud_status': server.cloud_status,
|
'cloud_status': server.cloud_status,
|
||||||
'connection_params': connection_params,
|
'connection_params': connection_params,
|
||||||
'connection_string': display_connection_str,
|
'connection_string': display_connection_str,
|
||||||
'prepare_threshold': server.prepare_threshold
|
'prepare_threshold': server.prepare_threshold,
|
||||||
|
'tags': tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
return ajax_response(response)
|
return ajax_response(response)
|
||||||
@ -1180,7 +1234,8 @@ class ServerNode(PGChildNodeView):
|
|||||||
passexec_expiration=data.get('passexec_expiration', None),
|
passexec_expiration=data.get('passexec_expiration', None),
|
||||||
kerberos_conn=1 if data.get('kerberos_conn', False) else 0,
|
kerberos_conn=1 if data.get('kerberos_conn', False) else 0,
|
||||||
connection_params=connection_params,
|
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.add(server)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -1273,7 +1328,8 @@ class ServerNode(PGChildNodeView):
|
|||||||
manager and manager.gss_authenticated else False,
|
manager and manager.gss_authenticated else False,
|
||||||
is_password_saved=bool(server.save_password),
|
is_password_saved=bool(server.save_password),
|
||||||
is_tunnel_password_saved=tunnel_password_saved,
|
is_tunnel_password_saved=tunnel_password_saved,
|
||||||
user_id=server.user_id
|
user_id=server.user_id,
|
||||||
|
tags=data.get('tags', None)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,6 +15,35 @@ import {default as supportedServers} from 'pgadmin.server.supported_servers';
|
|||||||
import current_user from 'pgadmin.user_management.current_user';
|
import current_user from 'pgadmin.user_management.current_user';
|
||||||
import { isEmptyString } from 'sources/validators';
|
import { isEmptyString } from 'sources/validators';
|
||||||
import VariableSchema from './variable.ui';
|
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 {
|
export default class ServerSchema extends BaseUISchema {
|
||||||
constructor(serverGroupOptions=[], userId=0, initValues={}) {
|
constructor(serverGroupOptions=[], userId=0, initValues={}) {
|
||||||
@ -50,11 +79,13 @@ export default class ServerSchema extends BaseUISchema {
|
|||||||
connection_params: [
|
connection_params: [
|
||||||
{'name': 'sslmode', 'value': 'prefer', 'keyword': 'sslmode'},
|
{'name': 'sslmode', 'value': 'prefer', 'keyword': 'sslmode'},
|
||||||
{'name': 'connect_timeout', 'value': 10, 'keyword': 'connect_timeout'}],
|
{'name': 'connect_timeout', 'value': 10, 'keyword': 'connect_timeout'}],
|
||||||
|
tags: [],
|
||||||
...initValues,
|
...initValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.serverGroupOptions = serverGroupOptions;
|
this.serverGroupOptions = serverGroupOptions;
|
||||||
this.paramSchema = new VariableSchema(this.getConnectionParameters(), null, null, ['name', 'keyword', 'value']);
|
this.paramSchema = new VariableSchema(this.getConnectionParameters(), null, null, ['name', 'keyword', 'value']);
|
||||||
|
this.tagsSchema = new TagsSchema();
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
_.bindAll(this, 'isShared');
|
_.bindAll(this, 'isShared');
|
||||||
}
|
}
|
||||||
@ -109,8 +140,8 @@ export default class ServerSchema extends BaseUISchema {
|
|||||||
{
|
{
|
||||||
id: 'bgcolor', label: gettext('Background'), type: 'color',
|
id: 'bgcolor', label: gettext('Background'), type: 'color',
|
||||||
group: null, mode: ['edit', 'create'],
|
group: null, mode: ['edit', 'create'],
|
||||||
disabled: obj.isConnected, deps: ['fgcolor'], depChange: (state)=>{
|
disabled: obj.isConnected, deps: ['fgcolor'], depChange: (state, source)=>{
|
||||||
if(!state.bgcolor && state.fgcolor) {
|
if(source[0] == 'fgcolor' && !state.bgcolor && state.fgcolor) {
|
||||||
return {'bgcolor': '#ffffff'};
|
return {'bgcolor': '#ffffff'};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -365,7 +396,13 @@ export default class ServerSchema extends BaseUISchema {
|
|||||||
mode: ['properties', 'edit', 'create'],
|
mode: ['properties', 'edit', 'create'],
|
||||||
helpMessageMode: ['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.')
|
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,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,6 +225,21 @@
|
|||||||
"expected_data": {
|
"expected_data": {
|
||||||
"status_code": 200
|
"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": [
|
"is_password_saved": [
|
||||||
|
@ -88,6 +88,9 @@ class AddServerTest(BaseTestGenerator):
|
|||||||
if 'bgcolor' in self.test_data:
|
if 'bgcolor' in self.test_data:
|
||||||
self.server['bgcolor'] = self.test_data['bgcolor']
|
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 self.is_positive_test:
|
||||||
if hasattr(self, 'with_save'):
|
if hasattr(self, 'with_save'):
|
||||||
self.server['save_password'] = self.with_save
|
self.server['save_password'] = self.with_save
|
||||||
|
@ -33,7 +33,7 @@ import config
|
|||||||
#
|
#
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
SCHEMA_VERSION = 40
|
SCHEMA_VERSION = 41
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
#
|
#
|
||||||
@ -209,6 +209,7 @@ class Server(db.Model):
|
|||||||
cloud_status = db.Column(db.Integer(), nullable=False, default=0)
|
cloud_status = db.Column(db.Integer(), nullable=False, default=0)
|
||||||
connection_params = db.Column(MutableDict.as_mutable(types.JSON))
|
connection_params = db.Column(MutableDict.as_mutable(types.JSON))
|
||||||
prepare_threshold = db.Column(db.Integer(), nullable=True)
|
prepare_threshold = db.Column(db.Integer(), nullable=True)
|
||||||
|
tags = db.Column(types.JSON)
|
||||||
|
|
||||||
|
|
||||||
class ModulePreference(db.Model):
|
class ModulePreference(db.Model):
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
useFieldOptions, useFieldValue, useFieldError, useSchemaStateSubscriber,
|
useFieldOptions, useFieldValue, useFieldError, useSchemaStateSubscriber,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
import { listenDepChanges } from './utils';
|
import { listenDepChanges } from './utils';
|
||||||
|
import { InputColor } from '../components/FormComponents';
|
||||||
|
|
||||||
|
|
||||||
/* Control mapping for form view */
|
/* Control mapping for form view */
|
||||||
@ -263,6 +264,8 @@ function MappedCellControlBase({
|
|||||||
return <InputDateTimePicker name={name} value={value} onChange={onTextChange} {...props}/>;
|
return <InputDateTimePicker name={name} value={value} onChange={onTextChange} {...props}/>;
|
||||||
case 'sql':
|
case 'sql':
|
||||||
return <InputSQL name={name} value={value} onChange={onSqlChange} {...props} />;
|
return <InputSQL name={name} value={value} onChange={onSqlChange} {...props} />;
|
||||||
|
case 'color':
|
||||||
|
return <InputColor name={name} value={value} onChange={onTextChange} {...props} />;
|
||||||
case 'file':
|
case 'file':
|
||||||
return <InputFileSelect name={name} value={value} onChange={onTextChange} inputRef={props.inputRef} {...props} />;
|
return <InputFileSelect name={name} value={value} onChange={onTextChange} inputRef={props.inputRef} {...props} />;
|
||||||
case 'keyCode':
|
case 'keyCode':
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
export default function reactAspenOverride(theme) {
|
export default function reactAspenOverride(theme) {
|
||||||
return {
|
return {
|
||||||
'.drag-tree-node': {
|
'.drag-tree-node': {
|
||||||
@ -47,7 +48,7 @@ export default function reactAspenOverride(theme) {
|
|||||||
top: '0px' + ' !important',
|
top: '0px' + ' !important',
|
||||||
|
|
||||||
'>div': {
|
'>div': {
|
||||||
scrollbarGutter: 'stable',
|
scrollbarGutter: 'auto',
|
||||||
overflow: 'overlay' + ' !important',
|
overflow: 'overlay' + ' !important',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -136,6 +137,7 @@ export default function reactAspenOverride(theme) {
|
|||||||
|
|
||||||
'span.file-label': {
|
'span.file-label': {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
gap: '2px',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '0 2px 0 2px',
|
padding: '0 2px 0 2px',
|
||||||
border: '1px solid transparent',
|
border: '1px solid transparent',
|
||||||
@ -153,13 +155,20 @@ export default function reactAspenOverride(theme) {
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
color: theme.otherVars.tree.textFg,
|
color: theme.otherVars.tree.textFg,
|
||||||
marginLeft: '3px',
|
|
||||||
cursor: 'pointer !important',
|
cursor: 'pointer !important',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
'&:hover, &.pseudo-active': {
|
'&:hover, &.pseudo-active': {
|
||||||
color: theme.otherVars.tree.fgHover,
|
color: theme.otherVars.tree.fgHover,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'div.file-tag': {
|
||||||
|
color: 'var(--tag-color)',
|
||||||
|
border: '1px solid color-mix(in srgb, var(--tag-color) 90%, #fff)',
|
||||||
|
padding: '0px 4px',
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
backgroundColor: 'color-mix(in srgb, color-mix(in srgb, var(--tag-color) 10%, #fff) 50%, transparent);',
|
||||||
|
lineHeight: 1.2
|
||||||
|
},
|
||||||
|
|
||||||
i: {
|
i: {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
@ -221,10 +230,5 @@ export default function reactAspenOverride(theme) {
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
'.children-count': {
|
|
||||||
marginLeft: '3px',
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -50,14 +50,6 @@ const Root = styled('div')(({theme}) => ({
|
|||||||
'& .Form-optionIcon': {
|
'& .Form-optionIcon': {
|
||||||
...theme.mixins.nodeIcon,
|
...theme.mixins.nodeIcon,
|
||||||
},
|
},
|
||||||
// '& .Form-label': {
|
|
||||||
// margin: theme.spacing(0.75, 0.75, 0.75, 0.75),
|
|
||||||
// display: 'flex',
|
|
||||||
// wordBreak: 'break-word'
|
|
||||||
// },
|
|
||||||
// '& .Form-labelError': {
|
|
||||||
// color: theme.palette.error.main,
|
|
||||||
// },
|
|
||||||
'& .Form-sql': {
|
'& .Form-sql': {
|
||||||
border: '1px solid ' + theme.otherVars.inputBorderColor,
|
border: '1px solid ' + theme.otherVars.inputBorderColor,
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
@ -72,6 +72,8 @@ export class FileTreeItem extends React.Component<IItemRendererXProps & IItemRen
|
|||||||
const itemChildren = item.children && item.children.length > 0 && item._metadata.data._type.indexOf('coll-') !== -1 ? '(' + item.children.length + ')' : '';
|
const itemChildren = item.children && item.children.length > 0 && item._metadata.data._type.indexOf('coll-') !== -1 ? '(' + item.children.length + ')' : '';
|
||||||
const extraClasses = item._metadata.data.extraClasses ? item._metadata.data.extraClasses.join(' ') : '';
|
const extraClasses = item._metadata.data.extraClasses ? item._metadata.data.extraClasses.join(' ') : '';
|
||||||
|
|
||||||
|
const tags = item._metadata.data?.tags ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('file-entry', {
|
className={cn('file-entry', {
|
||||||
@ -103,9 +105,13 @@ export class FileTreeItem extends React.Component<IItemRendererXProps & IItemRen
|
|||||||
}
|
}
|
||||||
<span className='file-name'>
|
<span className='file-name'>
|
||||||
{ _.unescape(this.props.item.getMetadata('data')._label)}
|
{ _.unescape(this.props.item.getMetadata('data')._label)}
|
||||||
<span className='children-count'>{itemChildren}</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
<span className='children-count'>{itemChildren}</span>
|
||||||
|
{tags.map((tag)=>(
|
||||||
|
<div key={tag.text} className='file-tag' style={{'--tag-color': tag.color} as React.CSSProperties}>
|
||||||
|
{tag.text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</span>
|
</span>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
@ -126,13 +126,13 @@ const defaultExtensions = [
|
|||||||
indentOnInput(),
|
indentOnInput(),
|
||||||
syntaxHighlighting,
|
syntaxHighlighting,
|
||||||
keymap.of([{
|
keymap.of([{
|
||||||
|
key: 'Tab',
|
||||||
|
run: acceptCompletion,
|
||||||
|
},{
|
||||||
key: 'Tab',
|
key: 'Tab',
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
run: insertTabWithUnit,
|
run: insertTabWithUnit,
|
||||||
shift: indentLess,
|
shift: indentLess,
|
||||||
},{
|
|
||||||
key: 'Tab',
|
|
||||||
run: acceptCompletion,
|
|
||||||
},{
|
},{
|
||||||
key: 'Backspace',
|
key: 'Backspace',
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
|
@ -15,7 +15,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { fullHexColor } from '../utils';
|
import { fullHexColor } from '../utils';
|
||||||
|
|
||||||
export function withColorPicker(Component) {
|
export function withColorPicker(Component) {
|
||||||
|
|
||||||
const HOCComponent = ({value, currObj, onChange, onSave, options, ...props})=>{
|
const HOCComponent = ({value, currObj, onChange, onSave, options, ...props})=>{
|
||||||
const pickrOptions = {
|
const pickrOptions = {
|
||||||
showPalette: true,
|
showPalette: true,
|
||||||
@ -74,10 +74,11 @@ export function withColorPicker(Component) {
|
|||||||
defaultRepresentation: pickrOptions.colorFormat,
|
defaultRepresentation: pickrOptions.colorFormat,
|
||||||
disabled: pickrOptions.disabled,
|
disabled: pickrOptions.disabled,
|
||||||
save: pickrOptions.allowSave,
|
save: pickrOptions.allowSave,
|
||||||
|
input: pickrOptions.input,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).on('init', instance => {
|
}).on('init', instance => {
|
||||||
setColor(value);
|
setColor(value, true);
|
||||||
pickrOptions.disabled && instance.disable();
|
pickrOptions.disabled && instance.disable();
|
||||||
|
|
||||||
const { lastColor } = instance.getRoot().preview;
|
const { lastColor } = instance.getRoot().preview;
|
||||||
|
@ -684,6 +684,10 @@ export function getChartColor(index, theme='light', colorPalette=CHART_THEME_COL
|
|||||||
return palette[index % palette.length];
|
return palette[index % palette.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRandomColor() {
|
||||||
|
return '#' + ((1 << 24) * Math.random() | 0).toString(16).padStart(6, '0');
|
||||||
|
}
|
||||||
|
|
||||||
// Using this function instead of 'btoa' directly.
|
// Using this function instead of 'btoa' directly.
|
||||||
// https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
// https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
||||||
function stringToBase64(str) {
|
function stringToBase64(str) {
|
||||||
|
@ -157,7 +157,7 @@ function SelectAllHeaderRenderer({isCellSelected}) {
|
|||||||
}, [isRowSelected]);
|
}, [isRowSelected]);
|
||||||
|
|
||||||
return <div ref={cellRef} style={{width: '100%', height: '100%'}} onClick={onClick}
|
return <div ref={cellRef} style={{width: '100%', height: '100%'}} onClick={onClick}
|
||||||
tabIndex="0" onKeyDown={dataGridExtras.handleShortcuts}></div>;
|
tabIndex="0" onKeyDown={(e)=>dataGridExtras.handleShortcuts(e, true)}></div>;
|
||||||
}
|
}
|
||||||
SelectAllHeaderRenderer.propTypes = {
|
SelectAllHeaderRenderer.propTypes = {
|
||||||
onAllRowsSelectionChange: PropTypes.func,
|
onAllRowsSelectionChange: PropTypes.func,
|
||||||
@ -192,7 +192,7 @@ function SelectableHeaderRenderer({column, selectedColumns, onSelectedColumnsCha
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box ref={cellRef} className={'QueryTool-columnHeader ' + (isSelected ? 'QueryTool-colHeaderSelected' : null)} onClick={onClick} tabIndex="0"
|
<Box ref={cellRef} className={'QueryTool-columnHeader ' + (isSelected ? 'QueryTool-colHeaderSelected' : null)} onClick={onClick} tabIndex="0"
|
||||||
onKeyDown={dataGridExtras.handleShortcuts} data-column-key={column.key}>
|
onKeyDown={(e)=>dataGridExtras.handleShortcuts(e, true)} data-column-key={column.key}>
|
||||||
{(column.column_type_internal == 'geometry' || column.column_type_internal == 'geography') &&
|
{(column.column_type_internal == 'geometry' || column.column_type_internal == 'geography') &&
|
||||||
<Box>
|
<Box>
|
||||||
<PgIconButton title={gettext('View all geometries in this column')} icon={<MapIcon data-label="MapIcon"/>} size="small" style={{marginRight: '0.25rem'}} onClick={(e)=>{
|
<PgIconButton title={gettext('View all geometries in this column')} icon={<MapIcon data-label="MapIcon"/>} size="small" style={{marginRight: '0.25rem'}} onClick={(e)=>{
|
||||||
@ -385,7 +385,13 @@ export default function QueryToolDataGrid({columns, rows, totalRowCount, dataCha
|
|||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_COPY_DATA);
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_COPY_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShortcuts(e) {
|
function handleShortcuts(e, withCopy=false) {
|
||||||
|
// Handle Copy shortcut Cmd/Ctrl + c
|
||||||
|
if((e.ctrlKey || e.metaKey) && e.key !== 'Control' && e.keyCode == 67 && withCopy) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCopy();
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Select All Cmd + A(mac) / Ctrl + a (others)
|
// Handle Select All Cmd + A(mac) / Ctrl + a (others)
|
||||||
if(((isMac() && e.metaKey) || (!isMac() && e.ctrlKey)) && e.key === 'a') {
|
if(((isMac() && e.metaKey) || (!isMac() && e.ctrlKey)) && e.key === 'a') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -528,6 +528,7 @@ def dump_database_servers(output_file, selected_servers,
|
|||||||
server.kerberos_conn),
|
server.kerberos_conn),
|
||||||
add_value(attr_dict, "ConnectionParameters",
|
add_value(attr_dict, "ConnectionParameters",
|
||||||
server.connection_params)
|
server.connection_params)
|
||||||
|
add_value(attr_dict, "Tags", server.tags)
|
||||||
|
|
||||||
# if desktop mode or server mode with
|
# if desktop mode or server mode with
|
||||||
# ENABLE_SERVER_PASS_EXEC_CMD flag is True
|
# ENABLE_SERVER_PASS_EXEC_CMD flag is True
|
||||||
@ -766,6 +767,8 @@ def load_database_servers(input_file, selected_servers,
|
|||||||
|
|
||||||
new_server.kerberos_conn = obj.get("KerberosAuthentication", None)
|
new_server.kerberos_conn = obj.get("KerberosAuthentication", None)
|
||||||
|
|
||||||
|
new_server.tags = obj.get("Tags", None)
|
||||||
|
|
||||||
# if desktop mode or server mode with
|
# if desktop mode or server mode with
|
||||||
# ENABLE_SERVER_PASS_EXEC_CMD flag is True
|
# ENABLE_SERVER_PASS_EXEC_CMD flag is True
|
||||||
if not current_app.config['SERVER_MODE'] or \
|
if not current_app.config['SERVER_MODE'] or \
|
||||||
|
Loading…
Reference in New Issue
Block a user