Skip to content

Commit e4d2ea9

Browse files
committed
GitHub Issue 847: App User Management page to allow users with manageUsersPermission to select / view all site users
- add "View All Site Users" manage button option to users admin page, based user.hasManageUsersPermission - when view all site users is selected, show the core.SiteUsers table instead of core.Users (which is only users with perm in project) - minor updates to page title and manage menu item text for application user options
1 parent 2d77730 commit e4d2ea9

4 files changed

Lines changed: 90 additions & 36 deletions

File tree

packages/components/releaseNotes/components.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version TBD
5+
*Released*: TBD
6+
- GitHub Issue 847: App User Management page to allow users with manageUsersPermission to select / view all site users
7+
48
### version 7.25.0
59
*Released*: 25 March 2026
610
- Update `isAllSamplesSchema` to account for move of `JobInputSamples` to `workflow` schema

packages/components/src/internal/components/user/UsersGridPanel.test.tsx

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ describe('<UsersGridPanel/>', () => {
5353
0,
5454
'user-management-users-inactive'
5555
),
56+
'user-management-users-site': makeTestQueryModel(
57+
SCHEMAS.CORE_TABLES.SITE_USERS,
58+
new QueryInfo({}),
59+
{},
60+
[],
61+
0,
62+
'user-management-users-site'
63+
),
5664
},
5765
searchParams: new URLSearchParams(),
5866
setSearchParams: jest.fn(),
@@ -64,19 +72,20 @@ describe('<UsersGridPanel/>', () => {
6472
renderWithAppContext(component);
6573
expect(document.querySelectorAll('.grid-panel')).toHaveLength(1);
6674
expect(document.querySelectorAll('.user-details-panel')).toHaveLength(1);
67-
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Application Active Users');
75+
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Active Application Users');
6876
const buttons = document.querySelectorAll('.grid-panel__button-bar-left button');
6977
expect(buttons).toHaveLength(4);
7078
expect(buttons[0].textContent).toBe('Create');
7179
expect(buttons[1].textContent).toBe('Manage');
7280
expect(document.querySelectorAll('.dropdown-toggle')[0].textContent.trim()).toEqual('Manage');
7381

7482
const menuItems = document.querySelectorAll('.lk-menu-item');
75-
expect(menuItems).toHaveLength(10);
83+
expect(menuItems).toHaveLength(11);
7684
expect(menuItems[0].textContent).toBe('Deactivate Users');
7785
expect(menuItems[1].textContent).toBe('Delete Users');
78-
expect(menuItems[2].textContent).toBe('View All Users');
79-
expect(menuItems[3].textContent).toBe('View Inactive Users');
86+
expect(menuItems[2].textContent).toBe('View All Application Users');
87+
expect(menuItems[3].textContent).toBe('View All Site Users');
88+
expect(menuItems[4].textContent).toBe('View Inactive Application Users');
8089
});
8190

8291
test('without delete or deactivate', () => {
@@ -85,7 +94,7 @@ describe('<UsersGridPanel/>', () => {
8594
renderWithAppContext(component);
8695
expect(document.querySelectorAll('.grid-panel')).toHaveLength(1);
8796
expect(document.querySelectorAll('.user-details-panel')).toHaveLength(1);
88-
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Application Active Users');
97+
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Active Application Users');
8998
const buttons = document.querySelectorAll('.grid-panel__button-bar-left button');
9099
expect(buttons).toHaveLength(4);
91100
expect(buttons[0].textContent).toBe('Create');
@@ -94,8 +103,8 @@ describe('<UsersGridPanel/>', () => {
94103

95104
const menuItems = document.querySelectorAll('.lk-menu-item');
96105
expect(menuItems).toHaveLength(8);
97-
expect(menuItems[0].textContent).toBe('View All Users');
98-
expect(menuItems[1].textContent).toBe('View Inactive Users');
106+
expect(menuItems[0].textContent).toBe('View All Application Users');
107+
expect(menuItems[1].textContent).toBe('View Inactive Application Users');
99108
});
100109

101110
test('without create, delete, or deactivate', () => {
@@ -104,16 +113,16 @@ describe('<UsersGridPanel/>', () => {
104113
renderWithAppContext(component);
105114
expect(document.querySelectorAll('.grid-panel')).toHaveLength(1);
106115
expect(document.querySelectorAll('.user-details-panel')).toHaveLength(1);
107-
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Application Active Users');
116+
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Active Application Users');
108117
const buttons = document.querySelectorAll('.grid-panel__button-bar-left button');
109118
expect(buttons).toHaveLength(3);
110119
expect(buttons[0].textContent).toBe('Manage');
111120
expect(document.querySelectorAll('.dropdown-toggle')[0].textContent.trim()).toEqual('Manage');
112121

113122
const menuItems = document.querySelectorAll('.lk-menu-item');
114123
expect(menuItems).toHaveLength(8);
115-
expect(menuItems[0].textContent).toBe('View All Users');
116-
expect(menuItems[1].textContent).toBe('View Inactive Users');
124+
expect(menuItems[0].textContent).toBe('View All Application Users');
125+
expect(menuItems[1].textContent).toBe('View Inactive Application Users');
117126
});
118127

119128
test('inactive users view', () => {
@@ -124,19 +133,20 @@ describe('<UsersGridPanel/>', () => {
124133
renderWithAppContext(component);
125134
expect(document.querySelectorAll('.grid-panel')).toHaveLength(1);
126135
expect(document.querySelectorAll('.user-details-panel')).toHaveLength(1);
127-
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Application Inactive Users');
136+
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Inactive Application Users');
128137
const buttons = document.querySelectorAll('.grid-panel__button-bar-left button');
129138
expect(buttons).toHaveLength(4);
130139
expect(buttons[0].textContent).toBe('Create');
131140
expect(buttons[1].textContent).toBe('Manage');
132141
expect(document.querySelectorAll('.dropdown-toggle')[0].textContent.trim()).toEqual('Manage');
133142

134143
const menuItems = document.querySelectorAll('.lk-menu-item');
135-
expect(menuItems).toHaveLength(10);
144+
expect(menuItems).toHaveLength(11);
136145
expect(menuItems[0].textContent).toBe('Delete Users');
137146
expect(menuItems[1].textContent).toBe('Reactivate Users');
138-
expect(menuItems[2].textContent).toBe('View Active Users');
139-
expect(menuItems[3].textContent).toBe('View All Users');
147+
expect(menuItems[2].textContent).toBe('View All Application Users');
148+
expect(menuItems[3].textContent).toBe('View All Site Users');
149+
expect(menuItems[4].textContent).toBe('View Active Application Users');
140150
});
141151

142152
test('all users view', () => {
@@ -147,18 +157,42 @@ describe('<UsersGridPanel/>', () => {
147157
renderWithAppContext(component);
148158
expect(document.querySelectorAll('.grid-panel')).toHaveLength(1);
149159
expect(document.querySelectorAll('.user-details-panel')).toHaveLength(1);
150-
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Application All Users');
160+
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Application Users');
151161
const buttons = document.querySelectorAll('.grid-panel__button-bar-left button');
152162
expect(buttons).toHaveLength(4);
153163
expect(buttons[0].textContent).toBe('Create');
154164
expect(buttons[1].textContent).toBe('Manage');
155165
expect(document.querySelectorAll('.dropdown-toggle')[0].textContent.trim()).toEqual('Manage');
156166

157167
const menuItems = document.querySelectorAll('.lk-menu-item');
158-
expect(menuItems).toHaveLength(9);
168+
expect(menuItems).toHaveLength(10);
159169
expect(menuItems[0].textContent).toBe('Delete Users');
160-
expect(menuItems[1].textContent).toBe('View Active Users');
161-
expect(menuItems[2].textContent).toBe('View Inactive Users');
170+
expect(menuItems[1].textContent).toBe('View All Site Users');
171+
expect(menuItems[2].textContent).toBe('View Active Application Users');
172+
expect(menuItems[3].textContent).toBe('View Inactive Application Users');
173+
});
174+
175+
test('site users view', () => {
176+
const component = (
177+
<UsersGridPanelImpl {...DEFAULT_PROPS} searchParams={new URLSearchParams({ usersView: 'site' })} />
178+
);
179+
180+
renderWithAppContext(component);
181+
expect(document.querySelectorAll('.grid-panel')).toHaveLength(1);
182+
expect(document.querySelectorAll('.user-details-panel')).toHaveLength(1);
183+
expect(document.querySelectorAll('.view-header')[0].textContent).toBe('Site Users');
184+
const buttons = document.querySelectorAll('.grid-panel__button-bar-left button');
185+
expect(buttons).toHaveLength(4);
186+
expect(buttons[0].textContent).toBe('Create');
187+
expect(buttons[1].textContent).toBe('Manage');
188+
expect(document.querySelectorAll('.dropdown-toggle')[0].textContent.trim()).toEqual('Manage');
189+
190+
const menuItems = document.querySelectorAll('.lk-menu-item');
191+
expect(menuItems).toHaveLength(10);
192+
expect(menuItems[0].textContent).toBe('Delete Users');
193+
expect(menuItems[1].textContent).toBe('View All Application Users');
194+
expect(menuItems[2].textContent).toBe('View Active Application Users');
195+
expect(menuItems[3].textContent).toBe('View Inactive Application Users');
162196
});
163197

164198
test('active user limit reached', () => {
@@ -176,11 +210,12 @@ describe('<UsersGridPanel/>', () => {
176210
expect(buttons[0].hasAttribute('disabled')).toBe(true);
177211

178212
const menuItems = document.querySelectorAll('.lk-menu-item');
179-
expect(menuItems).toHaveLength(10);
213+
expect(menuItems).toHaveLength(11);
180214
expect(menuItems[0].textContent).toBe('Delete Users');
181215
expect(menuItems[1].textContent).toBe('Reactivate Users');
182-
expect(menuItems[2].textContent).toBe('View Active Users');
183-
expect(menuItems[3].textContent).toBe('View All Users');
216+
expect(menuItems[2].textContent).toBe('View All Application Users');
217+
expect(menuItems[3].textContent).toBe('View All Site Users');
218+
expect(menuItems[4].textContent).toBe('View Active Application Users');
184219
});
185220

186221
test('active user limit not reached', () => {
@@ -198,11 +233,12 @@ describe('<UsersGridPanel/>', () => {
198233
expect(buttons[0].hasAttribute('disabled')).toBe(false);
199234

200235
const menuItems = document.querySelectorAll('.lk-menu-item');
201-
expect(menuItems).toHaveLength(10);
236+
expect(menuItems).toHaveLength(11);
202237
expect(menuItems[0].textContent).toBe('Delete Users');
203238
expect(menuItems[1].textContent).toBe('Reactivate Users');
204-
expect(menuItems[2].textContent).toBe('View Active Users');
205-
expect(menuItems[3].textContent).toBe('View All Users');
239+
expect(menuItems[2].textContent).toBe('View All Application Users');
240+
expect(menuItems[3].textContent).toBe('View All Site Users');
241+
expect(menuItems[4].textContent).toBe('View Active Application Users');
206242
});
207243

208244
test('active user limit disabled', () => {

packages/components/src/internal/components/user/UsersGridPanel.tsx

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ interface State {
7676
selectedUserId: number;
7777
// valid options are 'create', 'deactivate', 'reactivate', 'delete', undefined
7878
showDialog: string;
79-
// valid options are 'active', 'inactive', 'all'
79+
// valid options are 'active', 'inactive', 'all', 'site'
8080
usersView: string;
8181
}
8282

@@ -121,14 +121,16 @@ export class UsersGridPanelImpl extends PureComponent<Props, State> {
121121
}
122122

123123
initQueryModel = (usersView: string): void => {
124-
const { actions, container } = this.props;
125-
const baseFilters = usersView === 'all' ? [] : [Filter.create('active', usersView === 'active')];
124+
const { actions, container, user } = this.props;
125+
// GitHub Issue 847: is user has manageUsersPermission allow them to select / view all site users
126+
const schemaQuery = usersView === 'site' && user.hasManageUsersPermission() ? SCHEMAS.CORE_TABLES.SITE_USERS : SCHEMAS.CORE_TABLES.USERS;
127+
const baseFilters = usersView === 'all' || usersView === 'site' ? [] : [Filter.create('active', usersView === 'active')];
126128

127129
actions.addModel(
128130
{
129131
id: this.getUsersModelId(),
130132
containerPath: container.path,
131-
schemaQuery: SCHEMAS.CORE_TABLES.USERS,
133+
schemaQuery,
132134
baseFilters,
133135
omittedColumns: OMITTED_COLUMNS,
134136
bindURL: true,
@@ -142,7 +144,7 @@ export class UsersGridPanelImpl extends PureComponent<Props, State> {
142144
};
143145

144146
getUsersView(paramVal: string): string {
145-
return paramVal === 'inactive' || paramVal === 'all' ? paramVal : 'active'; // default to view active users
147+
return paramVal === 'inactive' || paramVal === 'all' || paramVal === 'site' ? paramVal : 'active'; // default to view active application users
146148
}
147149

148150
getUsersModelId(): string {
@@ -271,7 +273,7 @@ export class UsersGridPanelImpl extends PureComponent<Props, State> {
271273
Create
272274
</DisableableButton>
273275
)}
274-
<ManageDropdownButton showIcon={false}>
276+
<ManageDropdownButton showIcon={false} pullRight={false}>
275277
{user.hasManageUsersPermission() && usersView === 'active' && (
276278
<SelectionMenuItem
277279
text="Deactivate Users"
@@ -300,14 +302,17 @@ export class UsersGridPanelImpl extends PureComponent<Props, State> {
300302
nounPlural="users"
301303
/>
302304
)}
303-
{usersView !== 'active' && (
304-
<MenuItem onClick={() => this.toggleViewActive('active')}>View Active Users</MenuItem>
305-
)}
306305
{usersView !== 'all' && (
307-
<MenuItem onClick={() => this.toggleViewActive('all')}>View All Users</MenuItem>
306+
<MenuItem onClick={() => this.toggleViewActive('all')}>View All Application Users</MenuItem>
307+
)}
308+
{user.hasManageUsersPermission() && usersView !== 'site' && (
309+
<MenuItem onClick={() => this.toggleViewActive('site')}>View All Site Users</MenuItem>
310+
)}
311+
{usersView !== 'active' && (
312+
<MenuItem onClick={() => this.toggleViewActive('active')}>View Active Application Users</MenuItem>
308313
)}
309314
{usersView !== 'inactive' && (
310-
<MenuItem onClick={() => this.toggleViewActive('inactive')}>View Inactive Users</MenuItem>
315+
<MenuItem onClick={() => this.toggleViewActive('inactive')}>View Inactive Application Users</MenuItem>
311316
)}
312317
</ManageDropdownButton>
313318
</div>
@@ -322,6 +327,14 @@ export class UsersGridPanelImpl extends PureComponent<Props, State> {
322327
// don't pass container from this.props as we want to check serverContext.container
323328
const isAppHome = isAppHomeFolder();
324329

330+
let title = 'Application Users';
331+
if (user.hasManageUsersPermission() && usersView === 'site') {
332+
title = 'Site Users';
333+
}
334+
else if (usersView !== 'all') {
335+
title = capitalizeFirstChar(usersView) + ' Application Users'
336+
}
337+
325338
return (
326339
<>
327340
<div className="row">
@@ -332,7 +345,7 @@ export class UsersGridPanelImpl extends PureComponent<Props, State> {
332345
actions={actions}
333346
model={model}
334347
loadOnMount={false}
335-
title={'Application ' + capitalizeFirstChar(usersView) + ' Users'}
348+
title={title}
336349
ButtonsComponent={() => this.renderButtons()}
337350
highlightLastSelectedRow
338351
showChartMenu={false}

packages/components/src/internal/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const CORE_SCHEMA = 'core';
5555
export const CORE_TABLES = {
5656
SCHEMA: CORE_SCHEMA,
5757
DATA_STATES: new SchemaQuery(CORE_SCHEMA, 'DataStates'),
58+
SITE_USERS: new SchemaQuery(CORE_SCHEMA, 'SiteUsers'),
5859
USERS: new SchemaQuery(CORE_SCHEMA, 'Users'),
5960
USER_API_KEYS: new SchemaQuery(CORE_SCHEMA, 'UserApiKeys'),
6061
};

0 commit comments

Comments
 (0)