Skip to content

Commit 9ab451e

Browse files
Open user management in a separate tab instead of a dialog to enhance UI/UX. #8574
1 parent 213be44 commit 9ab451e

15 files changed

Lines changed: 785 additions & 517 deletions

File tree

web/pgadmin/browser/templates/browser/js/utils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ define('pgadmin.browser.utils',
156156
{% endif %}
157157
{% if is_admin %}
158158
{
159-
label: '{{ _('Users') }}',
159+
label: '{{ _('User Management') }}',
160160
type: 'normal',
161161
callback: ()=>{
162-
pgAdmin.UserManagement.show_users()
162+
pgAdmin.UserManagement.launchUserManagement()
163163
}
164164
},
165165
{

web/pgadmin/static/js/Theme/index.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,12 @@ basicSettings = createTheme(basicSettings, {
206206
height: '100%',
207207
boxSizing: 'border-box',
208208
},
209+
adornedStart: {
210+
paddingLeft: basicSettings.spacing(0.75),
211+
},
212+
inputAdornedStart: {
213+
paddingLeft: '2px',
214+
},
209215
adornedEnd: {
210216
paddingRight: basicSettings.spacing(0.75),
211217
},
@@ -523,7 +529,7 @@ function getFinalTheme(baseTheme) {
523529
},
524530
inputSizeSmall: {
525531
height: '16px', // + 12px of padding = 28px;
526-
}
532+
},
527533
}
528534
},
529535
MuiSelect: {

web/pgadmin/static/js/components/PgReactTableStyled.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,11 @@ export function getCheckboxHeaderCell({title}) {
441441
return Cell;
442442
}
443443

444-
export function getEditCell({isDisabled, title}) {
444+
export function getEditCell({isDisabled, title, onClick}) {
445445
const Cell = ({ row }) => {
446446
return <PgIconButton data-test="expand-row" title={title} icon={<EditRoundedIcon fontSize="small" />} className='pgrt-cell-button'
447447
onClick={()=>{
448-
row.toggleExpanded();
448+
onClick ? onClick(row) : row.toggleExpanded();
449449
}} disabled={isDisabled?.(row)}
450450
/>;
451451
};

web/pgadmin/static/js/components/PgTable.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import gettext from 'sources/gettext';
3737
import EmptyPanelMessage from './EmptyPanelMessage';
3838
import { InputText } from './FormComponents';
3939
import { PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader, PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent, getCheckboxCell, getCheckboxHeaderCell } from './PgReactTableStyled';
40+
import SearchRoundedIcon from '@mui/icons-material/SearchRounded';
4041

4142

4243
const ROW_HEIGHT = 30;
@@ -334,6 +335,7 @@ export default function PgTable({ caveTable = true, tableNoBorder = true, tableN
334335
onChange={(val) => {
335336
setSearchVal(val);
336337
}}
338+
startAdornment={<SearchRoundedIcon />}
337339
/>
338340
</Box>
339341
</Box>}

web/pgadmin/tools/user_management/__init__.py

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def get_exposed_url_endpoints(self):
6767
current_app.login_manager.login_view,
6868
'user_management.auth_sources', 'user_management.change_owner',
6969
'user_management.shared_servers', 'user_management.admin_users',
70-
'user_management.save'
70+
'user_management.save', 'user_management.save_id'
7171
]
7272

7373

@@ -168,7 +168,8 @@ def user(uid):
168168
'active': u.active,
169169
'role': u.roles[0].id,
170170
'auth_source': u.auth_source,
171-
'locked': u.locked
171+
'locked': u.locked,
172+
'canDrop': u.id != current_user.id
172173
})
173174

174175
res = users_data
@@ -337,7 +338,6 @@ def admin_users(uid=None):
337338

338339
return make_json_response(
339340
success=1,
340-
info=_("No shared servers found"),
341341
data={
342342
'status': 'success',
343343
'msg': 'Admin user list',
@@ -398,36 +398,32 @@ def auth_sources():
398398

399399

400400
@blueprint.route('/save', methods=['POST'], endpoint='save')
401+
@blueprint.route('/save/<int:id>', methods=['DELETE'], endpoint='save_id')
401402
@roles_required('Administrator')
402-
def save():
403+
def save(id=None):
403404
"""
404405
This function is used to add/update/delete users.
405406
"""
407+
if request.method == 'DELETE':
408+
status, res = delete_user(id)
409+
if not status:
410+
return internal_server_error(errormsg=res)
411+
412+
return ajax_response(
413+
status=200
414+
)
415+
406416
data = request.form if request.form else json.loads(
407417
request.data
408418
)
409419

410-
try:
411-
# Delete Users
412-
if 'deleted' in data:
413-
for item in data['deleted']:
414-
status, res = delete_user(item['id'])
415-
if not status:
416-
return internal_server_error(errormsg=res)
417-
# Create Users
418-
if 'added' in data:
419-
for item in data['added']:
420-
status, res = create_user(item)
421-
if not status:
422-
return internal_server_error(errormsg=res)
423-
# Modify Users
424-
if 'changed' in data:
425-
for item in data['changed']:
426-
status, res = update_user(item['id'], item)
427-
if not status:
428-
return internal_server_error(errormsg=res)
429-
except Exception as e:
430-
return internal_server_error(errormsg=str(e))
420+
if 'id' not in data:
421+
status, res = create_user(data)
422+
else:
423+
status, res = update_user(data['id'], data)
424+
425+
if not status:
426+
return internal_server_error(errormsg=res)
431427

432428
return ajax_response(
433429
status=200
@@ -468,9 +464,25 @@ def validate_password(data, new_data):
468464
raise InternalServerError(_("Passwords do not match."))
469465

470466

467+
def validate_unique_user(data):
468+
if 'username' not in data:
469+
return
470+
471+
exist_users = User.query.filter_by(
472+
username=data['username'],
473+
auth_source=data['auth_source']
474+
).count()
475+
476+
if exist_users != 0:
477+
raise InternalServerError(_("User email/username must be unique "
478+
"for an authentication source."))
479+
480+
471481
def validate_user(data):
472482
new_data = dict()
473483

484+
validate_unique_user(data)
485+
474486
validate_password(data, new_data)
475487

476488
if 'email' in data and data['email'] and data['email'] != "":
@@ -508,20 +520,12 @@ def _create_new_user(new_data):
508520
:param new_data: Data from user creation.
509521
:return: Return new created user.
510522
"""
511-
auth_source = new_data['auth_source'] if 'auth_source' in new_data \
512-
else INTERNAL
513-
username = new_data['username'] if \
514-
'username' in new_data and auth_source != \
515-
INTERNAL else new_data['email']
516-
email = new_data['email'] if 'email' in new_data else None
517-
password = new_data['password'] if 'password' in new_data else None
518-
519-
usr = User(username=username,
520-
email=email,
523+
usr = User(username=new_data['username'],
524+
email=new_data['email'],
521525
roles=new_data['roles'],
522526
active=new_data['active'],
523-
password=password,
524-
auth_source=auth_source)
527+
password=new_data['password'],
528+
auth_source=new_data['auth_source'])
525529
db.session.add(usr)
526530
db.session.commit()
527531
# Add default server group for new user.
@@ -544,8 +548,18 @@ def create_user(data):
544548
else:
545549
return False, _("Missing field: '{0}'").format(f)
546550

551+
data['auth_source'] = data['auth_source'] if 'auth_source' in data \
552+
else INTERNAL
553+
data['username'] = data['username'] if \
554+
'username' in data and data['auth_source'] != \
555+
INTERNAL else data['email']
556+
data['email'] = data['email'] if 'email' in data else None
557+
data['password'] = data['password'] if 'password' in data else None
558+
547559
try:
548560
new_data = validate_user(data)
561+
new_data['password'] = new_data['password']\
562+
if 'password' in new_data else None
549563

550564
if 'roles' in new_data:
551565
new_data['roles'] = [Role.query.get(new_data['roles'])]
@@ -588,7 +602,7 @@ def update_user(uid, data):
588602
if 'roles' in new_data:
589603
new_data['roles'] = [Role.query.get(new_data['roles'])]
590604
except Exception as e:
591-
return False, str(e.description)
605+
return False, str(e)
592606

593607
try:
594608
for k, v in new_data.items():
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/////////////////////////////////////////////////////////////
2+
//
3+
// pgAdmin 4 - PostgreSQL Tools
4+
//
5+
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
6+
// This software is released under the PostgreSQL Licence
7+
//
8+
//////////////////////////////////////////////////////////////
9+
10+
import React from 'react';
11+
import { Box, styled, Tab, Tabs } from '@mui/material';
12+
import TabPanel from '../../../../static/js/components/TabPanel';
13+
import Users from './Users';
14+
15+
const Root = styled('div')(({theme}) => ({
16+
height: '100%',
17+
background: theme.palette.grey[400],
18+
display: 'flex',
19+
flexDirection: 'column',
20+
padding: '8px',
21+
22+
'& .Component-panel': {
23+
flexGrow: 1,
24+
display: 'flex',
25+
flexDirection: 'column',
26+
...theme.mixins.panelBorder.all,
27+
}
28+
}));
29+
30+
export default function Component() {
31+
const [tabValue, setTabValue] = React.useState(0);
32+
33+
return (
34+
<Root>
35+
<Box className='Component-panel'>
36+
<Box>
37+
<Tabs
38+
value={tabValue}
39+
onChange={(_e, selTabValue) => {
40+
setTabValue(selTabValue);
41+
}}
42+
variant="scrollable"
43+
scrollButtons="auto"
44+
action={(ref)=>ref?.updateIndicator()}
45+
>
46+
<Tab label="Users" />
47+
</Tabs>
48+
</Box>
49+
<TabPanel value={tabValue} index={0}>
50+
<Users />
51+
</TabPanel>
52+
</Box>
53+
</Root>
54+
);
55+
}

0 commit comments

Comments
 (0)