Rearrange users edit and details page (#24116)

Co-authored-by: Mohamed OULD HOCINE <106236152+gally47@users.noreply.github.com>
This commit is contained in:
Laura Bergenthal-Grotlüschen
2025-11-06 11:39:22 +01:00
committed by GitHub
parent cea75cf77d
commit 52c105c6b6
5 changed files with 161 additions and 67 deletions

View File

@@ -0,0 +1,5 @@
type = "c" # One of: a(dded), c(hanged), d(eprecated), r(emoved), f(ixed), s(ecurity)
message = "Restructure layout of users edit and details view."
issues = ["graylog-plugin-enterprise#12002"]
pulls = ["24116"]

View File

@@ -17,6 +17,7 @@
import * as React from 'react';
import * as Immutable from 'immutable';
import { render, screen, waitFor } from 'wrappedTestingLibrary';
import userEvent from '@testing-library/user-event';
import { paginatedShares } from 'fixtures/sharedEntities';
import { reader as assignedRole } from 'fixtures/roles';
@@ -78,7 +79,8 @@ describe('UserDetails', () => {
describe('user settings', () => {
it('should display timezone', async () => {
render(<UserDetails user={user} />);
const tab = await screen.findByLabelText(/Preferences/i);
userEvent.click(tab);
await waitFor(() => {
if (!user.timezone) throw Error('timezone must be defined for provided user');
@@ -91,24 +93,36 @@ describe('UserDetails', () => {
const exampleUser = user.toBuilder().sessionTimeoutMs(10000).build();
render(<UserDetails user={exampleUser} />);
const tab = await screen.findByLabelText(/Preferences/i);
userEvent.click(tab);
await screen.findByText('10 Seconds');
});
it('for minutes', async () => {
render(<UserDetails user={user.toBuilder().sessionTimeoutMs(600000).build()} />);
const tab = await screen.findByLabelText(/Preferences/i);
userEvent.click(tab);
await screen.findByText('10 Minutes');
});
it('for hours', async () => {
render(<UserDetails user={user.toBuilder().sessionTimeoutMs(36000000).build()} />);
const tab = await screen.findByLabelText(/Preferences/i);
userEvent.click(tab);
await screen.findByText('10 Hours');
});
it('for days', async () => {
render(<UserDetails user={user.toBuilder().sessionTimeoutMs(864000000).build()} />);
const tab = await screen.findByLabelText(/Preferences/i);
userEvent.click(tab);
await screen.findByText('10 Days');
});
});
@@ -118,6 +132,9 @@ describe('UserDetails', () => {
it('should display assigned roles', async () => {
render(<UserDetails user={user} />);
const tab = await screen.findByLabelText(/Teams & Roles/i);
userEvent.click(tab);
await screen.findByText(assignedRole.name);
});
});
@@ -126,6 +143,9 @@ describe('UserDetails', () => {
it('should display info if license is not present', async () => {
render(<UserDetails user={user} />);
const tab = await screen.findByLabelText(/Teams & Roles/i);
userEvent.click(tab);
await screen.findAllByText(/Enterprise Feature/);
});
});

View File

@@ -14,11 +14,12 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import React, { useState } from 'react';
import { Col, Row, SegmentedControl } from 'components/bootstrap';
import { IfPermitted, Spinner } from 'components/common';
import type User from 'logic/users/User';
import SectionGrid from 'components/common/Section/SectionGrid';
import { isPermitted } from 'util/PermissionsMixin';
import TelemetrySettingsDetails from 'logic/telemetry/TelemetrySettingsDetails';
import useCurrentUser from 'hooks/useCurrentUser';
import TelemetrySettingsConfig from 'logic/telemetry/TelemetrySettingsConfig';
@@ -37,8 +38,33 @@ type Props = {
user: User | null | undefined;
};
export type UserSegment = 'profile' | 'settings_preferences' | 'collections' | 'teams_roles' | 'shared_entities';
export const editableUserSegments: Array<{ value: UserSegment; label: string }> = [
{ value: 'profile', label: 'Profile' },
{ value: 'settings_preferences', label: 'Preferences' },
{ value: 'teams_roles', label: 'Teams & Roles' },
];
const UserDetails = ({ user }: Props) => {
const currentUser = useCurrentUser();
const userSegments: Array<{ value: UserSegment; label: string }> = [
...editableUserSegments,
{ value: 'collections', label: 'Collections' },
{ value: 'shared_entities', label: 'Shared Entities' },
];
const editPermissionRequiredSections = ['profile', 'settings_preferences', 'teams_roles', 'collections'];
const filteredUserSegments = () => {
if (isPermitted(currentUser.permissions, `users:edit:${user?.username}`)) {
return userSegments;
}
return userSegments.filter((userSegment) => editPermissionRequiredSections.includes(userSegment.value));
};
const [selectedSegment, useSelectedSegment] = useState<UserSegment>(filteredUserSegments()[0].value);
const isLocalAdmin = currentUser.id === 'local:admin';
if (!user) {
@@ -46,17 +72,25 @@ const UserDetails = ({ user }: Props) => {
}
return (
<>
<SectionGrid>
<IfPermitted permissions={`users:edit:${user.username}`}>
<div>
<ProfileSection user={user} />
<Row className="content">
<Col md={12}>
<SegmentedControl<UserSegment> data={userSegments} value={selectedSegment} onChange={useSelectedSegment} />
</Col>
<Col md={12}>
{selectedSegment === 'profile' && <ProfileSection user={user} />}
{selectedSegment === 'settings_preferences' && (
<>
<IfPermitted permissions="*">
<SettingsSection user={user} />
</IfPermitted>
<PreferencesSection user={user} />
</div>
<div>
{currentUser.id === user.id && !isLocalAdmin && <TelemetrySettingsDetails />}
{currentUser.id === user.id && isLocalAdmin && <TelemetrySettingsConfig />}
</>
)}
{selectedSegment === 'teams_roles' && (
<>
<PermissionsUpdateInfo />
<IfPermitted permissions={`users:rolesedit:${user.username}`}>
<RolesSection user={user} />
@@ -64,24 +98,12 @@ const UserDetails = ({ user }: Props) => {
<IfPermitted permissions={`team:edit:${user.username}`}>
<TeamsSection user={user} />
</IfPermitted>
{currentUser.id === user.id && !isLocalAdmin && (
<IfPermitted permissions={`users:edit:${user.username}`}>
<TelemetrySettingsDetails />
</IfPermitted>
)}
<IfPermitted permissions={`users:edit:${user.username}`}>
<CollectionsSection user={user} />
</IfPermitted>
{currentUser.id === user.id && isLocalAdmin && (
<IfPermitted permissions={`users:edit:${user.username}`}>
<TelemetrySettingsConfig />
</IfPermitted>
)}
</div>
</IfPermitted>
</SectionGrid>
<SharedEntitiesSection userId={user.id} />
</>
</>
)}
{selectedSegment === 'collections' && <CollectionsSection user={user} />}
{selectedSegment === 'shared_entities' && <SharedEntitiesSection userId={user.id} />}
</Col>
</Row>
);
};

View File

@@ -16,15 +16,18 @@
*/
import React from 'react';
import { screen, render, act } from 'wrappedTestingLibrary';
import userEvent from '@testing-library/user-event';
import { adminUser, bob } from 'fixtures/users';
import UserEdit from './UserEdit';
jest.mock('./ProfileSection', () => () => <div>ProfileSection</div>);
jest.mock('./PreferencesSection', () => () => <div>PreferencesSection</div>);
jest.mock('./SettingsSection', () => () => <div>SettingsSection</div>);
jest.mock('./PasswordSection', () => () => <div>PasswordSection</div>);
jest.mock('./RolesSection', () => () => <div>RolesSection</div>);
jest.mock('./TeamsSection', () => () => <div>TeamsSection</div>);
jest.useFakeTimers();
@@ -53,15 +56,33 @@ describe('<UserEdit />', () => {
expect(screen.queryByText('Profile')).not.toBeInTheDocument();
});
it('should display profile, settings and password section', () => {
it('should display profile and password section', () => {
render(<UserEdit user={user} />);
expect(screen.getByText('ProfileSection')).toBeInTheDocument();
expect(screen.getByText('SettingsSection')).toBeInTheDocument();
expect(screen.getByText('RolesSection')).toBeInTheDocument();
expect(screen.getByText('PasswordSection')).toBeInTheDocument();
});
it('should display settings and preferences section', async () => {
render(<UserEdit user={user} />);
const tab = await screen.findByLabelText(/Preferences/i);
userEvent.click(tab);
expect(screen.getByText('PreferencesSection')).toBeInTheDocument();
expect(screen.getByText('SettingsSection')).toBeInTheDocument();
});
it('should display roles and teams section', async () => {
render(<UserEdit user={user} />);
const tab = await screen.findByLabelText(/Teams & Roles/i);
userEvent.click(tab);
expect(screen.getByText('RolesSection')).toBeInTheDocument();
expect(screen.getByText('TeamsSection')).toBeInTheDocument();
});
describe('external user', () => {
it('should not render profile section for external user', () => {
render(<UserEdit user={bob} />);

View File

@@ -14,12 +14,12 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import React, { useState } from 'react';
import UsersDomain from 'domainActions/users/UsersDomain';
import useCurrentUser from 'hooks/useCurrentUser';
import { Col, Row, SegmentedControl, Alert } from 'components/bootstrap';
import { Spinner, IfPermitted } from 'components/common';
import { Alert } from 'components/bootstrap';
import SectionComponent from 'components/common/Section/SectionComponent';
import type User from 'logic/users/User';
import { CurrentUserStore } from 'stores/users/CurrentUserStore';
@@ -33,8 +33,9 @@ import PreferencesSection from './PreferencesSection';
import RolesSection from './RolesSection';
import TeamsSection from './TeamsSection';
import type { UserSegment } from '../UserDetails/UserDetails';
import { editableUserSegments } from '../UserDetails/UserDetails';
import PermissionsUpdateInfo from '../PermissionsUpdateInfo';
import SectionGrid from '../../common/Section/SectionGrid';
type Props = {
user: User;
@@ -49,6 +50,7 @@ const _updateUser = (data, currentUser, userId, fullName) =>
const UserEdit = ({ user }: Props) => {
const currentUser = useCurrentUser();
const [selectedSegment, useSelectedSegment] = useState<UserSegment>('profile');
if (!user) {
return <Spinner />;
@@ -59,42 +61,66 @@ const UserEdit = ({ user }: Props) => {
}
return (
<SectionGrid>
<IfPermitted permissions={`users:edit:${user.username}`}>
<div>
{user.external && (
<SectionComponent title="External User">
<Alert bsStyle="warning">
This user was synced from an external server, therefore neither the profile nor the password can be
changed. Please contact your administrator for more information.
</Alert>
</SectionComponent>
<Row className="content">
<Col md={12}>
<SegmentedControl<UserSegment>
data={editableUserSegments}
value={selectedSegment}
onChange={useSelectedSegment}
/>
</Col>
<Col md={12}>
<IfPermitted permissions={`users:edit:${user.username}`}>
{selectedSegment === 'profile' && (
<>
{user.external && (
<SectionComponent title="External User">
<Alert bsStyle="warning">
This user was synced from an external server, therefore neither the profile nor the password can be
changed. Please contact your administrator for more information.
</Alert>
</SectionComponent>
)}
{!user.external && (
<ProfileSection
user={user}
onSubmit={(data) => _updateUser(data, currentUser, user.id, user.fullName)}
/>
)}
<IfPermitted permissions={`users:passwordchange:${user.username}`}>
{!user.external && <PasswordSection user={user} />}
</IfPermitted>
</>
)}
{!user.external && (
<ProfileSection user={user} onSubmit={(data) => _updateUser(data, currentUser, user.id, user.fullName)} />
{selectedSegment === 'settings_preferences' && (
<>
<SettingsSection
user={user}
onSubmit={(data) => _updateUser(data, currentUser, user.id, user.fullName)}
/>
<PreferencesSection user={user} />
{currentUser.id === user.id && (
<IfPermitted permissions={`users:edit:${user.username}`}>
<TelemetrySettingsConfig />
</IfPermitted>
)}
</>
)}
<SettingsSection user={user} onSubmit={(data) => _updateUser(data, currentUser, user.id, user.fullName)} />
<IfPermitted permissions={`users:passwordchange:${user.username}`}>
{!user.external && <PasswordSection user={user} />}
</IfPermitted>
<PreferencesSection user={user} />
</div>
<div>
<PermissionsUpdateInfo />
<IfPermitted permissions={`users:rolesedit:${user.username}`}>
<RolesSection user={user} onSubmit={(data) => _updateUser(data, currentUser, user.id, user.fullName)} />
</IfPermitted>
<IfPermitted permissions="team:edit">
<TeamsSection user={user} />
</IfPermitted>
{currentUser.id === user.id && (
<IfPermitted permissions={`users:edit:${user.username}`}>
<TelemetrySettingsConfig />
</IfPermitted>
{selectedSegment === 'teams_roles' && (
<>
<PermissionsUpdateInfo />
<IfPermitted permissions={`users:rolesedit:${user.username}`}>
<RolesSection user={user} onSubmit={(data) => _updateUser(data, currentUser, user.id, user.fullName)} />
</IfPermitted>
)}
</div>
</IfPermitted>
</SectionGrid>
<IfPermitted permissions="team:edit">
<TeamsSection user={user} />
</IfPermitted>
</>
)}
</Col>
</Row>
);
};