mirror of
https://github.com/Graylog2/graylog2-server.git
synced 2026-03-13 09:32:21 +08:00
Rearrange users edit and details page (#24116)
Co-authored-by: Mohamed OULD HOCINE <106236152+gally47@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
cea75cf77d
commit
52c105c6b6
5
changelog/unreleased/pr-24116.toml
Normal file
5
changelog/unreleased/pr-24116.toml
Normal 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"]
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user