Open user management in a separate tab instead of a dialog to enhance UI/UX. #8574
parent
213be44e29
commit
9ab451e163
|
|
@ -156,10 +156,10 @@ define('pgadmin.browser.utils',
|
|||
{% endif %}
|
||||
{% if is_admin %}
|
||||
{
|
||||
label: '{{ _('Users') }}',
|
||||
label: '{{ _('User Management') }}',
|
||||
type: 'normal',
|
||||
callback: ()=>{
|
||||
pgAdmin.UserManagement.show_users()
|
||||
pgAdmin.UserManagement.launchUserManagement()
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -206,6 +206,12 @@ basicSettings = createTheme(basicSettings, {
|
|||
height: '100%',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
adornedStart: {
|
||||
paddingLeft: basicSettings.spacing(0.75),
|
||||
},
|
||||
inputAdornedStart: {
|
||||
paddingLeft: '2px',
|
||||
},
|
||||
adornedEnd: {
|
||||
paddingRight: basicSettings.spacing(0.75),
|
||||
},
|
||||
|
|
@ -523,7 +529,7 @@ function getFinalTheme(baseTheme) {
|
|||
},
|
||||
inputSizeSmall: {
|
||||
height: '16px', // + 12px of padding = 28px;
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
MuiSelect: {
|
||||
|
|
|
|||
|
|
@ -441,11 +441,11 @@ export function getCheckboxHeaderCell({title}) {
|
|||
return Cell;
|
||||
}
|
||||
|
||||
export function getEditCell({isDisabled, title}) {
|
||||
export function getEditCell({isDisabled, title, onClick}) {
|
||||
const Cell = ({ row }) => {
|
||||
return <PgIconButton data-test="expand-row" title={title} icon={<EditRoundedIcon fontSize="small" />} className='pgrt-cell-button'
|
||||
onClick={()=>{
|
||||
row.toggleExpanded();
|
||||
onClick ? onClick(row) : row.toggleExpanded();
|
||||
}} disabled={isDisabled?.(row)}
|
||||
/>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import gettext from 'sources/gettext';
|
|||
import EmptyPanelMessage from './EmptyPanelMessage';
|
||||
import { InputText } from './FormComponents';
|
||||
import { PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader, PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent, getCheckboxCell, getCheckboxHeaderCell } from './PgReactTableStyled';
|
||||
import SearchRoundedIcon from '@mui/icons-material/SearchRounded';
|
||||
|
||||
|
||||
const ROW_HEIGHT = 30;
|
||||
|
|
@ -334,6 +335,7 @@ export default function PgTable({ caveTable = true, tableNoBorder = true, tableN
|
|||
onChange={(val) => {
|
||||
setSearchVal(val);
|
||||
}}
|
||||
startAdornment={<SearchRoundedIcon />}
|
||||
/>
|
||||
</Box>
|
||||
</Box>}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class UserManagementModule(PgAdminModule):
|
|||
current_app.login_manager.login_view,
|
||||
'user_management.auth_sources', 'user_management.change_owner',
|
||||
'user_management.shared_servers', 'user_management.admin_users',
|
||||
'user_management.save'
|
||||
'user_management.save', 'user_management.save_id'
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -168,7 +168,8 @@ def user(uid):
|
|||
'active': u.active,
|
||||
'role': u.roles[0].id,
|
||||
'auth_source': u.auth_source,
|
||||
'locked': u.locked
|
||||
'locked': u.locked,
|
||||
'canDrop': u.id != current_user.id
|
||||
})
|
||||
|
||||
res = users_data
|
||||
|
|
@ -337,7 +338,6 @@ def admin_users(uid=None):
|
|||
|
||||
return make_json_response(
|
||||
success=1,
|
||||
info=_("No shared servers found"),
|
||||
data={
|
||||
'status': 'success',
|
||||
'msg': 'Admin user list',
|
||||
|
|
@ -398,36 +398,32 @@ def auth_sources():
|
|||
|
||||
|
||||
@blueprint.route('/save', methods=['POST'], endpoint='save')
|
||||
@blueprint.route('/save/<int:id>', methods=['DELETE'], endpoint='save_id')
|
||||
@roles_required('Administrator')
|
||||
def save():
|
||||
def save(id=None):
|
||||
"""
|
||||
This function is used to add/update/delete users.
|
||||
"""
|
||||
if request.method == 'DELETE':
|
||||
status, res = delete_user(id)
|
||||
if not status:
|
||||
return internal_server_error(errormsg=res)
|
||||
|
||||
return ajax_response(
|
||||
status=200
|
||||
)
|
||||
|
||||
data = request.form if request.form else json.loads(
|
||||
request.data
|
||||
)
|
||||
|
||||
try:
|
||||
# Delete Users
|
||||
if 'deleted' in data:
|
||||
for item in data['deleted']:
|
||||
status, res = delete_user(item['id'])
|
||||
if not status:
|
||||
return internal_server_error(errormsg=res)
|
||||
# Create Users
|
||||
if 'added' in data:
|
||||
for item in data['added']:
|
||||
status, res = create_user(item)
|
||||
if not status:
|
||||
return internal_server_error(errormsg=res)
|
||||
# Modify Users
|
||||
if 'changed' in data:
|
||||
for item in data['changed']:
|
||||
status, res = update_user(item['id'], item)
|
||||
if not status:
|
||||
return internal_server_error(errormsg=res)
|
||||
except Exception as e:
|
||||
return internal_server_error(errormsg=str(e))
|
||||
if 'id' not in data:
|
||||
status, res = create_user(data)
|
||||
else:
|
||||
status, res = update_user(data['id'], data)
|
||||
|
||||
if not status:
|
||||
return internal_server_error(errormsg=res)
|
||||
|
||||
return ajax_response(
|
||||
status=200
|
||||
|
|
@ -468,9 +464,25 @@ def validate_password(data, new_data):
|
|||
raise InternalServerError(_("Passwords do not match."))
|
||||
|
||||
|
||||
def validate_unique_user(data):
|
||||
if 'username' not in data:
|
||||
return
|
||||
|
||||
exist_users = User.query.filter_by(
|
||||
username=data['username'],
|
||||
auth_source=data['auth_source']
|
||||
).count()
|
||||
|
||||
if exist_users != 0:
|
||||
raise InternalServerError(_("User email/username must be unique "
|
||||
"for an authentication source."))
|
||||
|
||||
|
||||
def validate_user(data):
|
||||
new_data = dict()
|
||||
|
||||
validate_unique_user(data)
|
||||
|
||||
validate_password(data, new_data)
|
||||
|
||||
if 'email' in data and data['email'] and data['email'] != "":
|
||||
|
|
@ -508,20 +520,12 @@ def _create_new_user(new_data):
|
|||
:param new_data: Data from user creation.
|
||||
:return: Return new created user.
|
||||
"""
|
||||
auth_source = new_data['auth_source'] if 'auth_source' in new_data \
|
||||
else INTERNAL
|
||||
username = new_data['username'] if \
|
||||
'username' in new_data and auth_source != \
|
||||
INTERNAL else new_data['email']
|
||||
email = new_data['email'] if 'email' in new_data else None
|
||||
password = new_data['password'] if 'password' in new_data else None
|
||||
|
||||
usr = User(username=username,
|
||||
email=email,
|
||||
usr = User(username=new_data['username'],
|
||||
email=new_data['email'],
|
||||
roles=new_data['roles'],
|
||||
active=new_data['active'],
|
||||
password=password,
|
||||
auth_source=auth_source)
|
||||
password=new_data['password'],
|
||||
auth_source=new_data['auth_source'])
|
||||
db.session.add(usr)
|
||||
db.session.commit()
|
||||
# Add default server group for new user.
|
||||
|
|
@ -544,8 +548,18 @@ def create_user(data):
|
|||
else:
|
||||
return False, _("Missing field: '{0}'").format(f)
|
||||
|
||||
data['auth_source'] = data['auth_source'] if 'auth_source' in data \
|
||||
else INTERNAL
|
||||
data['username'] = data['username'] if \
|
||||
'username' in data and data['auth_source'] != \
|
||||
INTERNAL else data['email']
|
||||
data['email'] = data['email'] if 'email' in data else None
|
||||
data['password'] = data['password'] if 'password' in data else None
|
||||
|
||||
try:
|
||||
new_data = validate_user(data)
|
||||
new_data['password'] = new_data['password']\
|
||||
if 'password' in new_data else None
|
||||
|
||||
if 'roles' in new_data:
|
||||
new_data['roles'] = [Role.query.get(new_data['roles'])]
|
||||
|
|
@ -588,7 +602,7 @@ def update_user(uid, data):
|
|||
if 'roles' in new_data:
|
||||
new_data['roles'] = [Role.query.get(new_data['roles'])]
|
||||
except Exception as e:
|
||||
return False, str(e.description)
|
||||
return False, str(e)
|
||||
|
||||
try:
|
||||
for k, v in new_data.items():
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import { Box, styled, Tab, Tabs } from '@mui/material';
|
||||
import TabPanel from '../../../../static/js/components/TabPanel';
|
||||
import Users from './Users';
|
||||
|
||||
const Root = styled('div')(({theme}) => ({
|
||||
height: '100%',
|
||||
background: theme.palette.grey[400],
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '8px',
|
||||
|
||||
'& .Component-panel': {
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
...theme.mixins.panelBorder.all,
|
||||
}
|
||||
}));
|
||||
|
||||
export default function Component() {
|
||||
const [tabValue, setTabValue] = React.useState(0);
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<Box className='Component-panel'>
|
||||
<Box>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={(_e, selTabValue) => {
|
||||
setTabValue(selTabValue);
|
||||
}}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
action={(ref)=>ref?.updateIndicator()}
|
||||
>
|
||||
<Tab label="Users" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<Users />
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import SchemaView from '../../../../static/js/SchemaView';
|
||||
import BaseUISchema from '../../../../static/js/SchemaView/base_schema.ui';
|
||||
import gettext from 'sources/gettext';
|
||||
import url_for from 'sources/url_for';
|
||||
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
|
||||
import {AUTH_METHODS} from 'pgadmin.browser.constants';
|
||||
import current_user from 'pgadmin.user_management.current_user';
|
||||
import { isEmptyString } from '../../../../static/js/validators';
|
||||
import ErrorBoundary from '../../../../static/js/helpers/ErrorBoundary';
|
||||
import PropTypes from 'prop-types';
|
||||
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
|
||||
|
||||
class UserSchema extends BaseUISchema {
|
||||
constructor(options, pgAdmin) {
|
||||
super({
|
||||
auth_source: 'internal',
|
||||
role: 1,
|
||||
active: true,
|
||||
refreshBrowserTree: false
|
||||
});
|
||||
this.options = options;
|
||||
this.pgAdmin = pgAdmin;
|
||||
|
||||
this.authOnlyInternal = (current_user['auth_sources'].length == 1 &&
|
||||
current_user['auth_sources'].includes(AUTH_METHODS['INTERNAL']));
|
||||
}
|
||||
|
||||
deleteUser(deleteRow) {
|
||||
this.pgAdmin.Browser.notifier.confirm(
|
||||
gettext('Delete user?'),
|
||||
gettext('Are you sure you wish to delete this user?'),
|
||||
deleteRow,
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
isUserNameEnabled(state) {
|
||||
return this.isNew(state) && state.auth_source != AUTH_METHODS['INTERNAL'];
|
||||
}
|
||||
|
||||
isNotCurrentUser(state) {
|
||||
return state.id != current_user['id'];
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let obj = this;
|
||||
return [
|
||||
{
|
||||
id: 'auth_source', label: gettext('Authentication source'),
|
||||
type: (state) => {
|
||||
return {
|
||||
type: 'select',
|
||||
options: () => {
|
||||
if (obj.isNew(state)) {
|
||||
return Promise.resolve(obj.options.authSources.filter((s) => current_user['auth_sources'].includes(s.value)));
|
||||
}
|
||||
return Promise.resolve(obj.options.authSources);
|
||||
},
|
||||
optionsReloadBasis: obj.isNew(state)
|
||||
};
|
||||
},
|
||||
controlProps: {
|
||||
allowClear: false,
|
||||
openOnEnter: false,
|
||||
},
|
||||
readonly: function (state) {
|
||||
return !obj.isNew(state) || obj.authOnlyInternal;
|
||||
}
|
||||
}, {
|
||||
id: 'username', label: gettext('Username'), type: 'text',
|
||||
deps: ['auth_source'],
|
||||
depChange: (state) => {
|
||||
if (!obj.isUserNameEnabled(state)) {
|
||||
return { username: undefined };
|
||||
}
|
||||
},
|
||||
readonly: (state) => {
|
||||
return !obj.isUserNameEnabled(state);
|
||||
}
|
||||
}, {
|
||||
id: 'email', label: gettext('Email'), type: 'text',
|
||||
deps: ['id'],
|
||||
readonly: (state) => {
|
||||
if (obj.isNew(state)) {
|
||||
return false;
|
||||
} else {
|
||||
return !obj.isNotCurrentUser(state) || state.auth_source == AUTH_METHODS['INTERNAL'];
|
||||
}
|
||||
}
|
||||
}, {
|
||||
id: 'role', label: gettext('Role'), type: 'select',
|
||||
options: obj.options.roles,
|
||||
controlProps: {
|
||||
allowClear: false,
|
||||
openOnEnter: false,
|
||||
},
|
||||
readonly: (state) => {
|
||||
if (obj.isNew(state)) {
|
||||
return false;
|
||||
}
|
||||
return !obj.isNotCurrentUser(state);
|
||||
}
|
||||
}, {
|
||||
id: 'active', label: gettext('Active'), type: 'switch',
|
||||
readonly: (state) => {
|
||||
if (obj.isNew(state)) {
|
||||
return false;
|
||||
}
|
||||
return !obj.isNotCurrentUser(state);
|
||||
}
|
||||
}, {
|
||||
id: 'newPassword', label: gettext('New password'), type: 'password',
|
||||
deps: ['auth_source'], controlProps: {
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
visible: (state)=>obj.isNotCurrentUser(state) && state.auth_source == AUTH_METHODS['INTERNAL'],
|
||||
}, {
|
||||
id: 'confirmPassword', label: gettext('Confirm password'), type: 'password',
|
||||
deps: ['auth_source'], controlProps: {
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
visible: (state)=>obj.isNotCurrentUser(state) && state.auth_source == AUTH_METHODS['INTERNAL'],
|
||||
}, {
|
||||
id: 'locked', label: gettext('Locked'), type: 'switch',
|
||||
readonly: (state) => {
|
||||
return !state.locked;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
validate(state, setError) {
|
||||
let msg;
|
||||
let obj = this;
|
||||
let minPassLen = this.pgAdmin.password_length_min;
|
||||
if (obj.isUserNameEnabled(state) && isEmptyString(state.username)) {
|
||||
msg = gettext('Username cannot be empty');
|
||||
setError('username', msg);
|
||||
return true;
|
||||
} else {
|
||||
setError('username', null);
|
||||
}
|
||||
|
||||
if (state.auth_source == AUTH_METHODS['INTERNAL']) {
|
||||
let email_filter = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
if (isEmptyString(state.email)) {
|
||||
msg = gettext('Email cannot be empty');
|
||||
setError('email', msg);
|
||||
return true;
|
||||
} else if (!email_filter.test(state.email)) {
|
||||
msg = gettext('Invalid email address: %s', state.email);
|
||||
setError('email', msg);
|
||||
return true;
|
||||
} else {
|
||||
setError('email', null);
|
||||
}
|
||||
|
||||
if (obj.isNew(state) && isEmptyString(state.newPassword)) {
|
||||
msg = gettext('Password cannot be empty for user %s', state.email);
|
||||
setError('newPassword', msg);
|
||||
return true;
|
||||
} else if (state.newPassword?.length < minPassLen) {
|
||||
msg = gettext('Password must be at least %s characters for user %s', minPassLen, state.email);
|
||||
setError('newPassword', msg);
|
||||
return true;
|
||||
} else {
|
||||
setError('newPassword', null);
|
||||
}
|
||||
|
||||
if (obj.isNew(state) && isEmptyString(state.confirmPassword)) {
|
||||
msg = gettext('Confirm Password cannot be empty for user %s', state.email);
|
||||
setError('confirmPassword', msg);
|
||||
return true;
|
||||
} else {
|
||||
setError('confirmPassword', null);
|
||||
}
|
||||
|
||||
if (state.newPassword !== state.confirmPassword) {
|
||||
msg = gettext('Passwords do not match for user %s', state.email);
|
||||
setError('confirmPassword', msg);
|
||||
return true;
|
||||
} else {
|
||||
setError('confirmPassword', null);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default function UserDialog({user, options, onClose}) {
|
||||
const pgAdmin = usePgAdmin();
|
||||
const schema = useMemo(() => new UserSchema(options, pgAdmin), []);
|
||||
const isEdit = Boolean(user.id);
|
||||
const api = getApiInstance();
|
||||
|
||||
const onSaveClick = (_isNew, changeData)=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
try {
|
||||
api.post(url_for('user_management.save'), changeData)
|
||||
.then(()=>{
|
||||
pgAdmin.Browser.notifier.success(gettext('Users Saved Successfully'));
|
||||
resolve();
|
||||
onClose(null, true);
|
||||
})
|
||||
.catch((err)=>{
|
||||
reject(err instanceof Error ? err : Error(gettext('Something went wrong')));
|
||||
});
|
||||
} catch (error) {
|
||||
reject(Error(parseApiError(error)));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return <ErrorBoundary>
|
||||
<SchemaView
|
||||
formType={'dialog'}
|
||||
getInitData={()=>{ return Promise.resolve(user); }}
|
||||
schema={schema}
|
||||
viewHelperProps={{
|
||||
mode: isEdit ? 'edit' : 'create',
|
||||
}}
|
||||
onSave={onSaveClick}
|
||||
onClose={onClose}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={true}
|
||||
disableDialogHelp={true}
|
||||
isTabView={false}
|
||||
/>
|
||||
</ErrorBoundary>;
|
||||
}
|
||||
|
||||
UserDialog.propTypes = {
|
||||
user: PropTypes.object,
|
||||
options: PropTypes.object,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
|
@ -1,467 +0,0 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import SchemaView from '../../../../static/js/SchemaView';
|
||||
import BaseUISchema from '../../../../static/js/SchemaView/base_schema.ui';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import gettext from 'sources/gettext';
|
||||
import url_for from 'sources/url_for';
|
||||
import PropTypes from 'prop-types';
|
||||
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
|
||||
import {AUTH_METHODS} from 'pgadmin.browser.constants';
|
||||
import current_user from 'pgadmin.user_management.current_user';
|
||||
import { isEmptyString } from '../../../../static/js/validators';
|
||||
import { showChangeOwnership } from '../../../../static/js/Dialogs/index';
|
||||
import _ from 'lodash';
|
||||
|
||||
const StyledBox = styled(Box)(() => ({
|
||||
height: '100%',
|
||||
'& .UserManagementDialog-root': {
|
||||
padding: 0 + ' !important',
|
||||
}
|
||||
}));
|
||||
|
||||
class UserManagementCollection extends BaseUISchema {
|
||||
constructor() {
|
||||
super({
|
||||
id: undefined,
|
||||
username: undefined,
|
||||
email: undefined,
|
||||
active: true,
|
||||
role: '2',
|
||||
newPassword: undefined,
|
||||
confirmPassword: undefined,
|
||||
locked: false,
|
||||
auth_source: AUTH_METHODS['INTERNAL']
|
||||
});
|
||||
|
||||
this.authOnlyInternal = (current_user['auth_sources'].length == 1 &&
|
||||
current_user['auth_sources'].includes(AUTH_METHODS['INTERNAL']));
|
||||
}
|
||||
|
||||
setAuthSources(src) {
|
||||
this.authSources = src;
|
||||
}
|
||||
|
||||
setRoleOptions(src) {
|
||||
this.roleOptions = src;
|
||||
}
|
||||
|
||||
get idAttribute() {
|
||||
return 'id';
|
||||
}
|
||||
|
||||
isUserNameEnabled(state) {
|
||||
return !(this.authOnlyInternal || state.auth_source == AUTH_METHODS['INTERNAL']);
|
||||
}
|
||||
|
||||
isEditable(state) {
|
||||
return state.id != current_user['id'];
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let obj = this;
|
||||
return [
|
||||
{
|
||||
id: 'auth_source', label: gettext('Authentication source'),
|
||||
cell: (state)=> {
|
||||
return {
|
||||
cell: 'select',
|
||||
options: ()=> {
|
||||
if (obj.isNew(state)) {
|
||||
return Promise.resolve(obj.authSources.filter((s)=> current_user['auth_sources'].includes(s.value)));
|
||||
}
|
||||
return Promise.resolve(obj.authSources);
|
||||
},
|
||||
optionsReloadBasis: obj.isNew(state)
|
||||
};
|
||||
},
|
||||
minWidth: 110, width: 110,
|
||||
controlProps: {
|
||||
allowClear: false,
|
||||
openOnEnter: false,
|
||||
first_empty: false,
|
||||
},
|
||||
visible: function() {
|
||||
return !obj.authOnlyInternal;
|
||||
},
|
||||
editable: function(state) {
|
||||
return (obj.isNew(state) && !obj.authOnlyInternal);
|
||||
}
|
||||
}, {
|
||||
id: 'username', label: gettext('Username'), cell: 'text',
|
||||
minWidth: 90, width: 90,
|
||||
deps: ['auth_source'],
|
||||
depChange: (state)=>{
|
||||
if (obj.isUserNameEnabled(state) && obj.isNew(state) && !isEmptyString(obj.username)) {
|
||||
return {username: undefined};
|
||||
}
|
||||
},
|
||||
editable: (state)=> {
|
||||
return obj.isUserNameEnabled(state);
|
||||
}
|
||||
}, {
|
||||
id: 'email', label: gettext('Email'), cell: 'text',
|
||||
minWidth: 90, width: 90, deps: ['id'],
|
||||
editable: (state)=> {
|
||||
if (obj.isNew(state))
|
||||
return true;
|
||||
|
||||
return obj.isEditable(state) && state.auth_source != AUTH_METHODS['INTERNAL'];
|
||||
}
|
||||
}, {
|
||||
id: 'role', label: gettext('Role'),
|
||||
cell: () => ({
|
||||
cell: 'select',
|
||||
options: obj.roleOptions,
|
||||
controlProps: {
|
||||
allowClear: false,
|
||||
openOnEnter: false,
|
||||
first_empty: false,
|
||||
},
|
||||
}),
|
||||
minWidth: 95, width: 95,
|
||||
editable: (state)=> {
|
||||
return obj.isEditable(state);
|
||||
}
|
||||
}, {
|
||||
id: 'active', label: gettext('Active'), cell: 'switch', width: 60, enableResizing: false,
|
||||
editable: (state)=> {
|
||||
return obj.isEditable(state);
|
||||
}
|
||||
}, {
|
||||
id: 'newPassword', label: gettext('New password'), cell: 'password',
|
||||
minWidth: 90, width: 90, deps: ['auth_source'], controlProps: {
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
editable: (state)=> {
|
||||
return obj.isEditable(state) && state.auth_source == AUTH_METHODS['INTERNAL'];
|
||||
}
|
||||
}, {
|
||||
id: 'confirmPassword', label: gettext('Confirm password'), cell: 'password',
|
||||
minWidth: 90, width: 90, deps: ['auth_source'], controlProps: {
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
editable: (state)=> {
|
||||
return obj.isEditable(state) && state.auth_source == AUTH_METHODS['INTERNAL'];
|
||||
}
|
||||
}, {
|
||||
id: 'locked', label: gettext('Locked'), cell: 'switch', width: 60, enableResizing: false,
|
||||
editable: (state)=> {
|
||||
return state.locked;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
validate(state, setError) {
|
||||
let msg;
|
||||
let obj = this;
|
||||
let minPassLen = pgAdmin.password_length_min;
|
||||
if (obj.isUserNameEnabled(state) && isEmptyString(state.username)) {
|
||||
msg = gettext('Username cannot be empty');
|
||||
setError('username', msg);
|
||||
return true;
|
||||
} else {
|
||||
setError('username', null);
|
||||
}
|
||||
|
||||
if (state.auth_source != AUTH_METHODS['INTERNAL']) {
|
||||
if (obj.isNew(state) && obj.top?.sessData?.userManagement) {
|
||||
for (let user of obj.top.sessData.userManagement) {
|
||||
if (user?.id &&
|
||||
user.username.toLowerCase() == state.username.toLowerCase() &&
|
||||
user.auth_source == state.auth_source) {
|
||||
msg = gettext('User name \'%s\' already exists', state.username);
|
||||
setError('username', msg);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.auth_source == AUTH_METHODS['INTERNAL']) {
|
||||
let email_filter = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
if (isEmptyString(state.email)) {
|
||||
msg = gettext('Email cannot be empty');
|
||||
setError('email', msg);
|
||||
return true;
|
||||
} else if (!email_filter.test(state.email)) {
|
||||
msg = gettext('Invalid email address: %s', state.email);
|
||||
setError('email', msg);
|
||||
return true;
|
||||
} else {
|
||||
setError('email', null);
|
||||
}
|
||||
|
||||
if (obj.isNew(state) && obj.top?.sessData?.userManagement) {
|
||||
for (let user of obj.top.sessData.userManagement) {
|
||||
if (user?.id &&
|
||||
user.email?.toLowerCase() == state.email?.toLowerCase()) {
|
||||
msg = gettext('Email address \'%s\' already exists', state.email);
|
||||
setError('email', msg);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.isNew(state) && isEmptyString(state.newPassword)) {
|
||||
msg = gettext('Password cannot be empty for user %s', state.email);
|
||||
setError('newPassword', msg);
|
||||
return true;
|
||||
} else if (state.newPassword?.length < minPassLen) {
|
||||
msg = gettext('Password must be at least %s characters for user %s', minPassLen, state.email);
|
||||
setError('newPassword', msg);
|
||||
return true;
|
||||
} else {
|
||||
setError('newPassword', null);
|
||||
}
|
||||
|
||||
if (obj.isNew(state) && isEmptyString(state.confirmPassword)) {
|
||||
msg = gettext('Confirm Password cannot be empty for user %s', state.email);
|
||||
setError('confirmPassword', msg);
|
||||
return true;
|
||||
} else {
|
||||
setError('confirmPassword', null);
|
||||
}
|
||||
|
||||
if (state.newPassword !== state.confirmPassword) {
|
||||
msg = gettext('Passwords do not match for user %s', state.email);
|
||||
setError('confirmPassword', msg);
|
||||
return true;
|
||||
} else {
|
||||
setError('confirmPassword', null);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class UserManagementSchema extends BaseUISchema {
|
||||
constructor() {
|
||||
super({refreshBrowserTree: false});
|
||||
this.userManagementCollObj = new UserManagementCollection();
|
||||
this.changeOwnership = false;
|
||||
}
|
||||
|
||||
setAuthSources(src) {
|
||||
this.userManagementCollObj.setAuthSources(src);
|
||||
}
|
||||
|
||||
setRoleOptions(src) {
|
||||
this.userManagementCollObj.setRoleOptions(src);
|
||||
}
|
||||
|
||||
deleteUser(deleteRow) {
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
gettext('Delete user?'),
|
||||
gettext('Are you sure you wish to delete this user?'),
|
||||
deleteRow,
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let obj = this;
|
||||
const api = getApiInstance();
|
||||
return [
|
||||
{
|
||||
id: 'userManagement', label: '', type: 'collection',
|
||||
schema: obj.userManagementCollObj,
|
||||
canAdd: true, canDelete: true, isFullTab: true,
|
||||
addOnTop: true,
|
||||
canDeleteRow: (row)=>{
|
||||
return row['id'] != current_user['id'];
|
||||
},
|
||||
onDelete: (row, deleteRow)=> {
|
||||
if (_.isUndefined(row['id'])) {
|
||||
deleteRow();
|
||||
return;
|
||||
}
|
||||
let deletedUser = {'id': row['id'], 'name': !isEmptyString(row['email']) ? row['email'] : row['username']};
|
||||
api.get(url_for('user_management.shared_servers', {'uid': row['id']}))
|
||||
.then((res)=>{
|
||||
if (res.data?.data?.shared_servers > 0) {
|
||||
api.get(url_for('user_management.admin_users', {'uid': row['id']}))
|
||||
.then((result)=>{
|
||||
showChangeOwnership(gettext('Change ownership'),
|
||||
result?.data?.data?.result?.data,
|
||||
res?.data?.data?.shared_servers,
|
||||
deletedUser,
|
||||
()=> {
|
||||
this.changeOwnership = true;
|
||||
deleteRow();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err)=>{
|
||||
pgAdmin.Browser.notifier.error(parseApiError(err));
|
||||
});
|
||||
} else {
|
||||
obj.deleteUser(deleteRow);
|
||||
}
|
||||
})
|
||||
.catch((err)=>{
|
||||
pgAdmin.Browser.notifier.error(parseApiError(err));
|
||||
obj.deleteUser(deleteRow);
|
||||
});
|
||||
},
|
||||
canSearch: true
|
||||
},
|
||||
{
|
||||
id: 'refreshBrowserTree', visible: false, type: 'switch',
|
||||
mode: ['non_supported'],
|
||||
deps: ['userManagement'], depChange: ()=> {
|
||||
return { refreshBrowserTree: this.changeOwnership };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function UserManagementDialog({onClose}) {
|
||||
|
||||
const [authSources, setAuthSources] = React.useState([]);
|
||||
const [roles, setRoles] = React.useState([]);
|
||||
const api = getApiInstance();
|
||||
const schema = React.useRef(null);
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
api.get(url_for('user_management.auth_sources'))
|
||||
.then(res=>{
|
||||
setAuthSources(res.data);
|
||||
})
|
||||
.catch((err)=>{
|
||||
pgAdmin.Browser.notifier.error(err);
|
||||
});
|
||||
|
||||
api.get(url_for('user_management.roles'))
|
||||
.then(res=>{
|
||||
setRoles(res.data);
|
||||
})
|
||||
.catch((err)=>{
|
||||
pgAdmin.Browser.notifier.error(parseApiError(err));
|
||||
});
|
||||
} catch (error) {
|
||||
pgAdmin.Browser.notifier.error(parseApiError(error));
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const onSaveClick = (_isNew, changeData)=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
try {
|
||||
if (changeData['refreshBrowserTree']) {
|
||||
// Confirmation dialog to refresh the browser tree.
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
gettext('Object explorer tree refresh required'),
|
||||
gettext('The ownership of the shared server was changed or the shared server was deleted, so the object explorer tree refresh is required. Do you wish to refresh the tree?'),
|
||||
function () {
|
||||
pgAdmin.Browser.tree.destroy();
|
||||
},
|
||||
function () {
|
||||
return true;
|
||||
},
|
||||
gettext('Refresh'),
|
||||
gettext('Later')
|
||||
);
|
||||
}
|
||||
api.post(url_for('user_management.save'), changeData['userManagement'])
|
||||
.then(()=>{
|
||||
pgAdmin.Browser.notifier.success('Users Saved Successfully');
|
||||
resolve();
|
||||
onClose();
|
||||
})
|
||||
.catch((err)=>{
|
||||
reject(err instanceof Error ? err : Error(gettext('Something went wrong')));
|
||||
});
|
||||
} catch (error) {
|
||||
reject(parseApiError(error));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const authSourcesOptions = authSources.map((m)=>({
|
||||
label: m.label,
|
||||
value: m.value,
|
||||
}));
|
||||
|
||||
const roleOptions = roles.map((m) => ({
|
||||
label: m.name,
|
||||
value: m.id,
|
||||
}));
|
||||
|
||||
if (!schema.current)
|
||||
schema.current = new UserManagementSchema();
|
||||
|
||||
if(authSourcesOptions.length <= 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if(roleOptions.length <= 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
schema.current.setAuthSources(authSourcesOptions);
|
||||
schema.current.setRoleOptions(roleOptions);
|
||||
|
||||
const onDialogHelp = () => {
|
||||
window.open(
|
||||
url_for('help.static', { 'filename': 'user_management.html' }),
|
||||
'pgadmin_help'
|
||||
);
|
||||
};
|
||||
|
||||
return <StyledBox><SchemaView
|
||||
formType={'dialog'}
|
||||
getInitData={()=>{ return new Promise((resolve, reject)=>{
|
||||
api.get(url_for('user_management.users'))
|
||||
.then((res)=>{
|
||||
resolve({userManagement:res.data});
|
||||
})
|
||||
.catch((err)=>{
|
||||
reject(err instanceof Error ? err : Error(gettext('Something went wrong')));
|
||||
});
|
||||
}); }}
|
||||
schema={schema.current}
|
||||
viewHelperProps={{
|
||||
mode: 'edit',
|
||||
}}
|
||||
onSave={onSaveClick}
|
||||
onClose={onClose}
|
||||
onHelp={onDialogHelp}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={true}
|
||||
isTabView={false}
|
||||
formClassName='UserManagementDialog-root'
|
||||
/></StyledBox>;
|
||||
}
|
||||
|
||||
UserManagementDialog.propTypes = {
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
|
||||
export function showUserManagement() {
|
||||
const title = gettext('User Management');
|
||||
pgAdmin.Browser.notifier.showModal(title, (onClose) => {
|
||||
return <UserManagementDialog
|
||||
onClose={()=>{onClose();}}
|
||||
/>;
|
||||
},
|
||||
{ isFullScreen: false, isResizeable: true, showFullScreen: false, isFullWidth: true,
|
||||
dialogWidth: pgAdmin.Browser.stdW.lg, dialogHeight: pgAdmin.Browser.stdH.md, id: 'id-user-management'});
|
||||
}
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { getDeleteCell, getEditCell, getSwitchCell } from '../../../../static/js/components/PgReactTableStyled';
|
||||
import gettext from 'sources/gettext';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
|
||||
import PgTable from 'sources/components/PgTable';
|
||||
import url_for from 'sources/url_for';
|
||||
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
|
||||
import ErrorBoundary from '../../../../static/js/helpers/ErrorBoundary';
|
||||
import UserDialog from './UserDialog';
|
||||
import { Box } from '@mui/material';
|
||||
import Loader from 'sources/components/Loader';
|
||||
import {Add as AddIcon, SyncRounded, Help as HelpIcon} from '@mui/icons-material';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PgButtonGroup, PgIconButton } from '../../../../static/js/components/Buttons';
|
||||
import { showChangeOwnership } from '../../../../static/js/Dialogs';
|
||||
import { isEmptyString } from '../../../../static/js/validators';
|
||||
|
||||
function CustomHeader({updateUsers, options}) {
|
||||
return (
|
||||
<Box>
|
||||
<PgButtonGroup>
|
||||
<PgIconButton
|
||||
icon={<AddIcon style={{ height: '1.4rem' }} />}
|
||||
aria-label="Create User"
|
||||
title={gettext('Create User...')}
|
||||
onClick={() => {
|
||||
const panelTitle = gettext('Create User');
|
||||
const panelId = BROWSER_PANELS.USER_MANAGEMENT + '-new';
|
||||
pgAdmin.Browser.docker.default_workspace.openDialog({
|
||||
id: panelId,
|
||||
title: panelTitle,
|
||||
content: (
|
||||
<ErrorBoundary>
|
||||
<UserDialog
|
||||
options={options}
|
||||
user={{}}
|
||||
onClose={(_e, reload) => {
|
||||
pgAdmin.Browser.docker.default_workspace.close(panelId, true);
|
||||
reload && updateUsers();
|
||||
}}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}, pgAdmin.Browser.stdW.md, pgAdmin.Browser.stdH.lg);
|
||||
}}
|
||||
></PgIconButton>
|
||||
<PgIconButton
|
||||
icon={<SyncRounded style={{ height: '1.4rem' }} />}
|
||||
aria-label="Refresh"
|
||||
title={gettext('Refresh')}
|
||||
onClick={() => {
|
||||
updateUsers();
|
||||
}}
|
||||
></PgIconButton>
|
||||
<PgIconButton
|
||||
icon={<HelpIcon style={{height: '1.4rem'}}/>}
|
||||
aria-label="Help"
|
||||
title={gettext('Help')}
|
||||
onClick={() => {
|
||||
window.open(url_for('help.static', {'filename': 'user_management.html'}));
|
||||
}}
|
||||
></PgIconButton>
|
||||
</PgButtonGroup>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
CustomHeader.propTypes = {
|
||||
updateUsers: PropTypes.func,
|
||||
options: PropTypes.object,
|
||||
};
|
||||
|
||||
export default function Users() {
|
||||
const authSources = useRef([]);
|
||||
const roles = useRef([]);
|
||||
const [loading, setLoading] = React.useState('');
|
||||
const [tableData, setTableData] = React.useState([]);
|
||||
const [selectedRows, setSelectedRows] = React.useState({});
|
||||
const api = getApiInstance();
|
||||
|
||||
const onDeleteClick = (row) => {
|
||||
const deleteRow = async () => {
|
||||
setLoading(gettext('Deleting user...'));
|
||||
try {
|
||||
await api.delete(url_for('user_management.save_id', { id: row.original.id }));
|
||||
pgAdmin.Browser.notifier.success(gettext('User deleted successfully.'));
|
||||
updateList();
|
||||
} catch (error) {
|
||||
pgAdmin.Browser.notifier.error(parseApiError(error));
|
||||
}
|
||||
setLoading('');
|
||||
};
|
||||
|
||||
pgAdmin.Browser.notifier.confirm(gettext('Delete User'), gettext('Are you sure you want to delete the user %s?', row.original.username),
|
||||
async () => {
|
||||
setLoading(gettext('Deleting user...'));
|
||||
try {
|
||||
const resp = await api.get(url_for('user_management.shared_servers', {'uid': row['id']}));
|
||||
const noOfSharedServers = resp.data?.data?.shared_servers ?? 0;
|
||||
if (noOfSharedServers > 0) {
|
||||
const resp = await api.get(url_for('user_management.admin_users', {'uid': row['id']}));
|
||||
showChangeOwnership(
|
||||
gettext('Change ownership'),
|
||||
resp.data?.data?.result?.data,
|
||||
noOfSharedServers,
|
||||
{'id': row.original['id'], 'name': !isEmptyString(row.original['email']) ? row.original['email'] : row.original['username']},
|
||||
()=> {
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
gettext('Object explorer tree refresh required'),
|
||||
gettext('The ownership of the shared server was changed or the shared server was deleted, so the object explorer tree refresh is required. Do you wish to refresh the tree?'),
|
||||
function () {
|
||||
pgAdmin.Browser.tree.destroy();
|
||||
},
|
||||
function () {
|
||||
return true;
|
||||
},
|
||||
gettext('Refresh'),
|
||||
gettext('Later')
|
||||
);
|
||||
deleteRow();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
deleteRow();
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
pgAdmin.Browser.notifier.error(parseApiError(error));
|
||||
}
|
||||
setLoading('');
|
||||
});
|
||||
};
|
||||
|
||||
const onEditClick = (row) => {
|
||||
const user = row.original;
|
||||
const panelTitle = gettext('Edit User - %s', user.username);
|
||||
const panelId = BROWSER_PANELS.USER_MANAGEMENT + '-edit-' + user.id;
|
||||
pgAdmin.Browser.docker.default_workspace.openDialog({
|
||||
id: panelId,
|
||||
title: panelTitle,
|
||||
content: (
|
||||
<ErrorBoundary>
|
||||
<UserDialog
|
||||
options={{
|
||||
authSources: authSources.current.map((s) => ({ label: s.label, value: s.value })),
|
||||
roles: roles.current.map((r) => ({ label: r.name, value: r.id })),
|
||||
}}
|
||||
user={user}
|
||||
onClose={(_e, reload) => {
|
||||
pgAdmin.Browser.docker.default_workspace.close(panelId, true);
|
||||
reload && updateList();
|
||||
}}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}, pgAdmin.Browser.stdW.md, pgAdmin.Browser.stdH.lg);
|
||||
};
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [{
|
||||
header: () => null,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
enableFilters: false,
|
||||
size: 35,
|
||||
maxSize: 35,
|
||||
minSize: 35,
|
||||
id: 'btn-delete',
|
||||
cell: getDeleteCell({ title: gettext('Delete User'), onClick: onDeleteClick, isDisabled: (row) => !row.original.canDrop }),
|
||||
},{
|
||||
header: () => null,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
enableFilters: false,
|
||||
size: 35,
|
||||
maxSize: 35,
|
||||
minSize: 35,
|
||||
id: 'btn-edit',
|
||||
cell: getEditCell({ title: gettext('Edit User'), onClick: onEditClick }),
|
||||
},
|
||||
{
|
||||
header: gettext('Auth Source'),
|
||||
accessorFn: (row) => authSources.current.find((s)=>s.value == row.auth_source).label,
|
||||
enableSorting: true,
|
||||
enableResizing: true,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
enableFilters: true,
|
||||
},
|
||||
{
|
||||
header: gettext('Username'),
|
||||
accessorKey: 'username',
|
||||
enableSorting: true,
|
||||
enableResizing: true,
|
||||
size: 200,
|
||||
minSize: 150,
|
||||
enableFilters: true,
|
||||
},
|
||||
{
|
||||
header: gettext('Email'),
|
||||
accessorKey: 'email',
|
||||
enableSorting: true,
|
||||
enableResizing: true,
|
||||
size: 200,
|
||||
minSize: 150,
|
||||
enableFilters: true,
|
||||
},
|
||||
{
|
||||
header: gettext('Role'),
|
||||
accessorFn: (row) => roles.current.find((r)=>r.id == row.role).name,
|
||||
enableSorting: true,
|
||||
enableResizing: true,
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
enableFilters: true,
|
||||
},
|
||||
{
|
||||
header: gettext('Active'),
|
||||
accessorKey: 'active',
|
||||
enableSorting: true,
|
||||
enableResizing: true,
|
||||
size: 50,
|
||||
minSize: 50,
|
||||
enableFilters: true,
|
||||
cell: getSwitchCell(),
|
||||
},
|
||||
{
|
||||
header: gettext('Locked'),
|
||||
accessorKey: 'locked',
|
||||
enableSorting: true,
|
||||
enableResizing: true,
|
||||
size: 50,
|
||||
minSize: 50,
|
||||
enableFilters: true,
|
||||
cell: getSwitchCell(),
|
||||
}];
|
||||
}, []);
|
||||
|
||||
const updateList = async () => {
|
||||
setLoading(gettext('Fetching users...'));
|
||||
try {
|
||||
const res = await api.get(url_for('user_management.users'));
|
||||
setTableData(res.data);
|
||||
} catch (error) {
|
||||
pgAdmin.Browser.notifier.error(parseApiError(error));
|
||||
}
|
||||
setLoading('');
|
||||
};
|
||||
|
||||
const initialize = async () => {
|
||||
setLoading(gettext('Loading...'));
|
||||
try {
|
||||
const res = await Promise.all([
|
||||
api.get(url_for('user_management.auth_sources')),
|
||||
api.get(url_for('user_management.roles')),
|
||||
]);
|
||||
authSources.current = res[0].data;
|
||||
roles.current = res[1].data;
|
||||
updateList();
|
||||
} catch (error) {
|
||||
setLoading('');
|
||||
pgAdmin.Browser.notifier.error(parseApiError(error));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initialize();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{position: 'relative', height: '100%'}}>
|
||||
<Loader message={loading} />
|
||||
<PgTable
|
||||
data-test="users"
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
sortOptions={[{ id: 'username', desc: true }]}
|
||||
selectedRows={selectedRows}
|
||||
setSelectedRows={setSelectedRows}
|
||||
caveTable={false}
|
||||
tableNoBorder={false}
|
||||
tableProps={{
|
||||
getRowId: (row) => {
|
||||
return row.id;
|
||||
}
|
||||
}}
|
||||
customHeader={<CustomHeader updateUsers={updateList} options={{
|
||||
authSources: authSources.current.map((s) => ({ label: s.label, value: s.value })),
|
||||
roles: roles.current.map((r) => ({ label: r.name, value: r.id })),
|
||||
}} />}
|
||||
></PgTable>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,10 +7,12 @@
|
|||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import gettext from 'sources/gettext';
|
||||
import { showChangeUserPassword, showUrlDialog } from '../../../../static/js/Dialogs/index';
|
||||
import { showUserManagement } from './UserManagementDialog';
|
||||
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
|
||||
import Component from './Component';
|
||||
|
||||
class UserManagement {
|
||||
static instance;
|
||||
|
|
@ -38,9 +40,18 @@ class UserManagement {
|
|||
showUrlDialog(gettext('Authentication'), url, 'mfa.html', 1000, 600);
|
||||
}
|
||||
|
||||
// This is a callback function to show user management dialog.
|
||||
show_users() {
|
||||
showUserManagement();
|
||||
// This is a callback function to show user management tab.
|
||||
launchUserManagement() {
|
||||
pgAdmin.Browser.docker.default_workspace.openTab({
|
||||
id: BROWSER_PANELS.USER_MANAGEMENT,
|
||||
title: gettext('User Management'),
|
||||
content: <Component />,
|
||||
closable: true,
|
||||
cache: false,
|
||||
group: 'playground'
|
||||
}, BROWSER_PANELS.MAIN, 'middle', true);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -9,5 +9,6 @@
|
|||
|
||||
module.exports = {
|
||||
'id': 'pgadmin4@pgadmin.org',
|
||||
'current_auth_source': 'internal'
|
||||
'current_auth_source': 'internal',
|
||||
'auth_sources': ['internal'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,5 +38,8 @@ module.exports = {
|
|||
'bgprocess.detailed_status': '/misc/bgprocess/<pid>/<int:out>/<int:err>/',
|
||||
'bgprocess.list': '/misc/bgprocess/',
|
||||
'bgprocess.stop_process': '/misc/bgprocess/stop/<pid>',
|
||||
'bgprocess.acknowledge': '/misc/bgprocess/<pid>'
|
||||
'bgprocess.acknowledge': '/misc/bgprocess/<pid>',
|
||||
'user_management.auth_sources': '/user_management/auth_sources',
|
||||
'user_management.roles': '/user_management/roles',
|
||||
'user_management.users': '/user_management/users',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { act, render } from '@testing-library/react';
|
||||
import { withBrowser } from '../genericFunctions';
|
||||
import UserDialog from '../../../pgadmin/tools/user_management/static/js/UserDialog';
|
||||
|
||||
describe('UserDialog', ()=>{
|
||||
describe('Component', ()=>{
|
||||
const UserDialogWithBrowser = withBrowser(UserDialog);
|
||||
|
||||
it('init', async ()=>{
|
||||
let ctrl;
|
||||
await act(async ()=>{
|
||||
ctrl = await render(<UserDialogWithBrowser
|
||||
options={{
|
||||
authSources: [{id: 1, label: 'internal', value: 'internal'}],
|
||||
roles: [{value: 1, label: 'Administrator'}, {value: 2, label: 'User'}],
|
||||
}}
|
||||
user={{}}
|
||||
onClose={() => {
|
||||
// Intentionally left blank
|
||||
}}
|
||||
/>);
|
||||
});
|
||||
expect(ctrl.container.querySelector('.FormView-nonTabPanel .MuiFormLabel-root').textContent).toBe('Authentication source');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { act, render } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from 'axios';
|
||||
import { withBrowser } from '../genericFunctions';
|
||||
import Users from '../../../pgadmin/tools/user_management/static/js/Users';
|
||||
|
||||
describe('Users', ()=>{
|
||||
let networkMock;
|
||||
|
||||
beforeEach(()=>{
|
||||
networkMock = new MockAdapter(axios);
|
||||
networkMock.onGet('/user_management/auth_sources').reply(200,
|
||||
[{'id':1,'label':'internal','value':'internal'}]
|
||||
);
|
||||
networkMock.onGet('/user_management/roles').reply(200,
|
||||
[
|
||||
{'id':1,'name':'Administrator'},
|
||||
{'id':2,'name':'User'}
|
||||
],
|
||||
);
|
||||
networkMock.onGet('/user_management/users').reply(200,
|
||||
[{'id':1,'label':'postgres','value':'postgres', 'auth_source': 'internal', 'role': 1}],
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
networkMock.restore();
|
||||
});
|
||||
|
||||
describe('Component', ()=>{
|
||||
const UsersWithBrowser = withBrowser(Users);
|
||||
|
||||
it('init', async ()=>{
|
||||
let ctrl;
|
||||
await act(async ()=>{
|
||||
ctrl = await render(<UsersWithBrowser />);
|
||||
});
|
||||
expect(ctrl.container.querySelectorAll('[data-test="users"]').length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -155,7 +155,7 @@ let webpackShimConfig = {
|
|||
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js/'),
|
||||
'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js/'),
|
||||
'pgadmin.tools.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js/'),
|
||||
'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'),
|
||||
'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/'),
|
||||
'pgadmin.user_management.current_user': '/user_management/current_user',
|
||||
},
|
||||
externals: [
|
||||
|
|
|
|||
Loading…
Reference in New Issue