mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Added first UI components for user managment (#5875)
* Added first UI components for user managment Ref #4962 * Add user roles to table and serializer * added key to AddItem actions * added ordering to group * style text * do not show unnecessary options * fix admi / superuser usage * switched to use BooleanColumn * Added active column * added user role change action * added user active change action * Added api change log * fixed logical error * added admin center to navigation * added groups to user serializer * added groups to the uI * Added user drawer * fixed active state * remove actions as they are not usable after refactor * move functions to drawer * added drawer lock state * added edit toggle * merge fix * renamed values * remove empty roles section * fix settings header * make title shorter to reducelayout shift when switching to server settings
This commit is contained in:
parent
41296e4574
commit
33c02fcd78
@ -2,10 +2,15 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 149
|
||||
INVENTREE_API_VERSION = 150
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
v150 -> 2023-11-07: https://github.com/inventree/InvenTree/pull/5875
|
||||
- Extended user API endpoints to enable ordering
|
||||
- Extended user API endpoints to enable user role changes
|
||||
- Added endpoint to create a new user
|
||||
|
||||
v149 -> 2023-11-07 : https://github.com/inventree/InvenTree/pull/5876
|
||||
- Add 'building' quantity to BomItem serializer
|
||||
- Add extra ordering options for the BomItem list API
|
||||
|
@ -6,6 +6,7 @@ from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -15,7 +16,7 @@ from djmoney.contrib.django_rest_framework.fields import MoneyField
|
||||
from djmoney.money import Money
|
||||
from djmoney.utils import MONEY_CLASSES, get_currency_field_name
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.serializers import DecimalField
|
||||
from rest_framework.utils import model_meta
|
||||
@ -302,6 +303,72 @@ class UserSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class ExendedUserSerializer(UserSerializer):
|
||||
"""Serializer for a User with a bit more info."""
|
||||
from users.serializers import GroupSerializer
|
||||
|
||||
groups = GroupSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta(UserSerializer.Meta):
|
||||
"""Metaclass defines serializer fields."""
|
||||
fields = UserSerializer.Meta.fields + [
|
||||
'groups',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'is_active'
|
||||
]
|
||||
|
||||
read_only_fields = UserSerializer.Meta.read_only_fields + [
|
||||
'groups',
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Expanded validation for changing user role."""
|
||||
# Check if is_staff or is_superuser is in attrs
|
||||
role_change = 'is_staff' in attrs or 'is_superuser' in attrs
|
||||
request_user = self.context['request'].user
|
||||
|
||||
if role_change:
|
||||
if request_user.is_superuser:
|
||||
# Superusers can change any role
|
||||
pass
|
||||
elif request_user.is_staff and 'is_superuser' not in attrs:
|
||||
# Staff can change any role except is_superuser
|
||||
pass
|
||||
else:
|
||||
raise PermissionDenied(_("You do not have permission to change this user role."))
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class UserCreateSerializer(ExendedUserSerializer):
|
||||
"""Serializer for creating a new User."""
|
||||
def validate(self, attrs):
|
||||
"""Expanded valiadation for auth."""
|
||||
# Check that the user trying to create a new user is a superuser
|
||||
if not self.context['request'].user.is_superuser:
|
||||
raise serializers.ValidationError(_("Only superusers can create new users"))
|
||||
|
||||
# Generate a random password
|
||||
password = User.objects.make_random_password(length=14)
|
||||
attrs.update({'password': password})
|
||||
return super().validate(attrs)
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Send an e email to the user after creation."""
|
||||
instance = super().create(validated_data)
|
||||
|
||||
# Make sure the user cannot login until they have set a password
|
||||
instance.set_unusable_password()
|
||||
# Send the user an onboarding email (from current site)
|
||||
current_site = Site.objects.get_current()
|
||||
domain = current_site.domain
|
||||
instance.email_user(
|
||||
subject=_(f"Welcome to {current_site.name}"),
|
||||
message=_(f"Your account has been created.\n\nPlease use the password reset function to get access (at https://{domain})."),
|
||||
)
|
||||
return instance
|
||||
|
||||
|
||||
class InvenTreeAttachmentSerializerField(serializers.FileField):
|
||||
"""Override the DRF native FileField serializer, to remove the leading server path.
|
||||
|
||||
|
@ -6,14 +6,14 @@ import logging
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import exceptions, permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from InvenTree.filters import InvenTreeSearchFilter
|
||||
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateAPI
|
||||
from InvenTree.serializers import UserSerializer
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||
from InvenTree.mixins import (ListAPI, ListCreateAPI, RetrieveAPI,
|
||||
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
||||
from InvenTree.serializers import ExendedUserSerializer, UserCreateSerializer
|
||||
from users.models import ApiToken, Owner, RuleSet, check_user_role
|
||||
from users.serializers import GroupSerializer, OwnerSerializer
|
||||
|
||||
@ -112,11 +112,11 @@ class RoleDetails(APIView):
|
||||
return Response(data)
|
||||
|
||||
|
||||
class UserDetail(RetrieveAPI):
|
||||
class UserDetail(RetrieveUpdateDestroyAPI):
|
||||
"""Detail endpoint for a single user."""
|
||||
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
serializer_class = ExendedUserSerializer
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated
|
||||
]
|
||||
@ -130,19 +130,15 @@ class MeUserDetail(RetrieveUpdateAPI, UserDetail):
|
||||
return self.request.user
|
||||
|
||||
|
||||
class UserList(ListAPI):
|
||||
class UserList(ListCreateAPI):
|
||||
"""List endpoint for detail on all users."""
|
||||
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
serializer_class = UserCreateSerializer
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
InvenTreeSearchFilter,
|
||||
]
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
search_fields = [
|
||||
'first_name',
|
||||
@ -150,8 +146,18 @@ class UserList(ListAPI):
|
||||
'username',
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
'email',
|
||||
'username',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'is_active',
|
||||
]
|
||||
|
||||
class GroupDetail(RetrieveAPI):
|
||||
|
||||
class GroupDetail(RetrieveUpdateDestroyAPI):
|
||||
"""Detail endpoint for a particular auth group"""
|
||||
|
||||
queryset = Group.objects.all()
|
||||
@ -161,7 +167,7 @@ class GroupDetail(RetrieveAPI):
|
||||
]
|
||||
|
||||
|
||||
class GroupList(ListAPI):
|
||||
class GroupList(ListCreateAPI):
|
||||
"""List endpoint for all auth groups"""
|
||||
|
||||
queryset = Group.objects.all()
|
||||
@ -170,15 +176,16 @@ class GroupList(ListAPI):
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
InvenTreeSearchFilter,
|
||||
]
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
search_fields = [
|
||||
'name',
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
'name',
|
||||
]
|
||||
|
||||
|
||||
class GetAuthToken(APIView):
|
||||
"""Return authentication token for an authenticated user."""
|
||||
|
@ -1,18 +1,8 @@
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Space,
|
||||
Stack,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { Anchor, Group, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconSwitch } from '@tabler/icons-react';
|
||||
import { ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { StylishText } from '../items/StylishText';
|
||||
|
||||
/**
|
||||
* Construct a settings page header with interlinks to one other settings page
|
||||
*/
|
||||
@ -24,35 +14,28 @@ export function SettingsHeader({
|
||||
switch_text,
|
||||
switch_link
|
||||
}: {
|
||||
title: string;
|
||||
title: string | ReactNode;
|
||||
shorthand?: string;
|
||||
subtitle?: string;
|
||||
subtitle?: string | ReactNode;
|
||||
switch_condition?: boolean;
|
||||
switch_text?: string | ReactNode;
|
||||
switch_link?: string;
|
||||
}) {
|
||||
return (
|
||||
<Paper shadow="xs" radius="xs" p="xs">
|
||||
<Group position="apart">
|
||||
<Stack spacing="xs">
|
||||
<Group position="left" spacing="xs">
|
||||
<StylishText size="xl">{title}</StylishText>
|
||||
<Text size="sm">{shorthand}</Text>
|
||||
</Group>
|
||||
<Text italic>{subtitle}</Text>
|
||||
</Stack>
|
||||
<Space />
|
||||
<Stack spacing="0" ml={'sm'}>
|
||||
<Group>
|
||||
<Title order={3}>{title}</Title>
|
||||
{shorthand && <Text c="dimmed">({shorthand})</Text>}
|
||||
</Group>
|
||||
<Group>
|
||||
<Text c="dimmed">{subtitle}</Text>
|
||||
{switch_text && switch_link && switch_condition && (
|
||||
<Anchor component={Link} to={switch_link}>
|
||||
<Button variant="outline">
|
||||
<Group spacing="sm">
|
||||
<IconSwitch size={18} />
|
||||
<Text>{switch_text}</Text>
|
||||
</Group>
|
||||
</Button>
|
||||
<IconSwitch size={14} />
|
||||
{switch_text}
|
||||
</Anchor>
|
||||
)}
|
||||
</Group>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
102
src/frontend/src/components/tables/settings/GroupTable.tsx
Normal file
102
src/frontend/src/components/tables/settings/GroupTable.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Text } from '@mantine/core';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ApiPaths } from '../../../enums/ApiEndpoints';
|
||||
import {
|
||||
openCreateApiForm,
|
||||
openDeleteApiForm,
|
||||
openEditApiForm
|
||||
} from '../../../functions/forms';
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { apiUrl } from '../../../states/ApiState';
|
||||
import { AddItemButton } from '../../buttons/AddItemButton';
|
||||
import { TableColumn } from '../Column';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
|
||||
|
||||
/**
|
||||
* Table for displaying list of groups
|
||||
*/
|
||||
export function GroupTable() {
|
||||
const { tableKey, refreshTable } = useTableRefresh('groups');
|
||||
|
||||
const columns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'name',
|
||||
sortable: true,
|
||||
title: t`Name`
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const rowActions = useCallback((record: any): RowAction[] => {
|
||||
return [
|
||||
RowEditAction({
|
||||
onClick: () => {
|
||||
openEditApiForm({
|
||||
url: ApiPaths.group_list,
|
||||
pk: record.pk,
|
||||
title: t`Edit group`,
|
||||
fields: {
|
||||
name: {}
|
||||
},
|
||||
onFormSuccess: refreshTable,
|
||||
successMessage: t`Group updated`
|
||||
});
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
onClick: () => {
|
||||
openDeleteApiForm({
|
||||
url: ApiPaths.group_list,
|
||||
pk: record.pk,
|
||||
title: t`Delete group`,
|
||||
successMessage: t`Group deleted`,
|
||||
onFormSuccess: refreshTable,
|
||||
preFormContent: (
|
||||
<Text>{t`Are you sure you want to delete this group?`}</Text>
|
||||
)
|
||||
});
|
||||
}
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const addGroup = useCallback(() => {
|
||||
openCreateApiForm({
|
||||
url: ApiPaths.group_list,
|
||||
title: t`Add group`,
|
||||
fields: { name: {} },
|
||||
onFormSuccess: refreshTable,
|
||||
successMessage: t`Added group`
|
||||
});
|
||||
}, []);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
let actions = [];
|
||||
|
||||
actions.push(
|
||||
<AddItemButton
|
||||
key={'add-group'}
|
||||
onClick={addGroup}
|
||||
tooltip={t`Add group`}
|
||||
/>
|
||||
);
|
||||
|
||||
return actions;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiPaths.group_list)}
|
||||
tableKey={tableKey}
|
||||
columns={columns}
|
||||
props={{
|
||||
rowActions: rowActions,
|
||||
customActionGroups: tableActions
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
217
src/frontend/src/components/tables/settings/UserDrawer.tsx
Normal file
217
src/frontend/src/components/tables/settings/UserDrawer.tsx
Normal file
@ -0,0 +1,217 @@
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import {
|
||||
Chip,
|
||||
Drawer,
|
||||
Group,
|
||||
List,
|
||||
Loader,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useToggle } from '@mantine/hooks';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconCheck } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { api } from '../../../App';
|
||||
import { ApiPaths } from '../../../enums/ApiEndpoints';
|
||||
import {
|
||||
invalidResponse,
|
||||
permissionDenied
|
||||
} from '../../../functions/notifications';
|
||||
import { apiUrl } from '../../../states/ApiState';
|
||||
import { useUserState } from '../../../states/UserState';
|
||||
import { EditButton } from '../../items/EditButton';
|
||||
import { UserDetailI } from './UserTable';
|
||||
|
||||
export function UserDrawer({
|
||||
opened,
|
||||
close,
|
||||
refreshTable,
|
||||
userDetail
|
||||
}: {
|
||||
opened: boolean;
|
||||
close: () => void;
|
||||
refreshTable: () => void;
|
||||
userDetail: UserDetailI | undefined;
|
||||
}) {
|
||||
const [user] = useUserState((state) => [state.user]);
|
||||
const [rightsValue, setRightsValue] = useState(['']);
|
||||
const [locked, setLocked] = useState<boolean>(false);
|
||||
const [userEditing, setUserEditing] = useToggle([false, true] as const);
|
||||
|
||||
// Set initial values
|
||||
useEffect(() => {
|
||||
if (!userDetail) return;
|
||||
|
||||
setLocked(true);
|
||||
// rights
|
||||
let new_rights = [];
|
||||
if (userDetail.is_staff) {
|
||||
new_rights.push('is_staff');
|
||||
}
|
||||
if (userDetail.is_active) {
|
||||
new_rights.push('is_active');
|
||||
}
|
||||
if (userDetail.is_superuser) {
|
||||
new_rights.push('is_superuser');
|
||||
}
|
||||
setRightsValue(new_rights);
|
||||
|
||||
setLocked(false);
|
||||
}, [userDetail]);
|
||||
|
||||
// actions on role change
|
||||
function changeRights(roles: [string]) {
|
||||
if (!userDetail) return;
|
||||
|
||||
let data = {
|
||||
is_staff: roles.includes('is_staff'),
|
||||
is_superuser: roles.includes('is_superuser')
|
||||
};
|
||||
if (
|
||||
data.is_staff != userDetail.is_staff ||
|
||||
data.is_superuser != userDetail.is_superuser
|
||||
) {
|
||||
setPermission(userDetail.pk, data);
|
||||
}
|
||||
if (userDetail.is_active != roles.includes('is_active')) {
|
||||
setActive(userDetail.pk, roles.includes('is_active'));
|
||||
}
|
||||
setRightsValue(roles);
|
||||
}
|
||||
|
||||
function setPermission(pk: number, data: any) {
|
||||
setLocked(true);
|
||||
api
|
||||
.patch(`${apiUrl(ApiPaths.user_list)}${pk}/`, data)
|
||||
.then(() => {
|
||||
notifications.show({
|
||||
title: t`User permission changed successfully`,
|
||||
message: t`Some changes might only take effect after the user refreshes their login.`,
|
||||
color: 'green',
|
||||
icon: <IconCheck size="1rem" />
|
||||
});
|
||||
refreshTable();
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response.status === 403) {
|
||||
permissionDenied();
|
||||
} else {
|
||||
console.log(error);
|
||||
invalidResponse(error.response.status);
|
||||
}
|
||||
})
|
||||
.finally(() => setLocked(false));
|
||||
}
|
||||
|
||||
function setActive(pk: number, active: boolean) {
|
||||
setLocked(true);
|
||||
api
|
||||
.patch(`${apiUrl(ApiPaths.user_list)}${pk}/`, {
|
||||
is_active: active
|
||||
})
|
||||
.then(() => {
|
||||
notifications.show({
|
||||
title: t`Changed user active status successfully`,
|
||||
message: t`Set to ${active}`,
|
||||
color: 'green',
|
||||
icon: <IconCheck size="1rem" />
|
||||
});
|
||||
refreshTable();
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response.status === 403) {
|
||||
permissionDenied();
|
||||
} else {
|
||||
console.log(error);
|
||||
invalidResponse(error.response.status);
|
||||
}
|
||||
})
|
||||
.finally(() => setLocked(false));
|
||||
}
|
||||
|
||||
const userEditable = locked || !userEditing;
|
||||
return (
|
||||
<Drawer
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
position="right"
|
||||
title={userDetail ? t`User details for ${userDetail.username}` : ''}
|
||||
overlayProps={{ opacity: 0.5, blur: 4 }}
|
||||
>
|
||||
<Stack spacing={'xs'}>
|
||||
<Group>
|
||||
<Title order={5}>
|
||||
<Trans>Details</Trans>
|
||||
</Title>
|
||||
<EditButton
|
||||
editing={userEditing}
|
||||
setEditing={setUserEditing}
|
||||
disabled
|
||||
/>
|
||||
</Group>
|
||||
{userDetail ? (
|
||||
<Stack spacing={0} ml={'md'}>
|
||||
<TextInput
|
||||
label={t`Username`}
|
||||
value={userDetail.username}
|
||||
disabled={userEditable}
|
||||
/>
|
||||
<TextInput label={t`Email`} value={userDetail.email} disabled />
|
||||
<TextInput
|
||||
label={t`First Name`}
|
||||
value={userDetail.first_name}
|
||||
disabled={userEditable}
|
||||
/>
|
||||
<TextInput
|
||||
label={t`Last Name`}
|
||||
value={userDetail.last_name}
|
||||
disabled={userEditable}
|
||||
/>
|
||||
|
||||
<Text>
|
||||
<Trans>Rights</Trans>
|
||||
</Text>
|
||||
<Chip.Group multiple value={rightsValue} onChange={changeRights}>
|
||||
<Group spacing={0}>
|
||||
<Chip value="is_active" disabled={locked || !user?.is_staff}>
|
||||
<Trans>Active</Trans>
|
||||
</Chip>
|
||||
<Chip value="is_staff" disabled={locked || !user?.is_staff}>
|
||||
<Trans>Staff</Trans>
|
||||
</Chip>
|
||||
<Chip
|
||||
value="is_superuser"
|
||||
disabled={locked || !(user?.is_staff && user?.is_superuser)}
|
||||
>
|
||||
<Trans>Superuser</Trans>
|
||||
</Chip>
|
||||
</Group>
|
||||
</Chip.Group>
|
||||
</Stack>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
|
||||
<Title order={5}>
|
||||
<Trans>Groups</Trans>
|
||||
</Title>
|
||||
<Text ml={'md'}>
|
||||
{userDetail && userDetail.groups.length == 0 ? (
|
||||
<Trans>No groups</Trans>
|
||||
) : (
|
||||
<List>
|
||||
{userDetail &&
|
||||
userDetail.groups.map((message) => (
|
||||
<List.Item key={message.name}>{message.name}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
176
src/frontend/src/components/tables/settings/UserTable.tsx
Normal file
176
src/frontend/src/components/tables/settings/UserTable.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Text } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { ApiPaths } from '../../../enums/ApiEndpoints';
|
||||
import {
|
||||
openCreateApiForm,
|
||||
openDeleteApiForm,
|
||||
openEditApiForm
|
||||
} from '../../../functions/forms';
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { apiUrl } from '../../../states/ApiState';
|
||||
import { AddItemButton } from '../../buttons/AddItemButton';
|
||||
import { TableColumn } from '../Column';
|
||||
import { BooleanColumn } from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
|
||||
import { UserDrawer } from './UserDrawer';
|
||||
|
||||
interface GroupDetailI {
|
||||
pk: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UserDetailI {
|
||||
pk: number;
|
||||
username: string;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
groups: GroupDetailI[];
|
||||
is_active: boolean;
|
||||
is_staff: boolean;
|
||||
is_superuser: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table for displaying list of users
|
||||
*/
|
||||
export function UserTable() {
|
||||
const { tableKey, refreshTable } = useTableRefresh('users');
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [userDetail, setUserDetail] = useState<UserDetailI>();
|
||||
|
||||
const columns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'email',
|
||||
sortable: true,
|
||||
title: t`Email`
|
||||
},
|
||||
{
|
||||
accessor: 'username',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
title: t`Username`
|
||||
},
|
||||
{
|
||||
accessor: 'first_name',
|
||||
sortable: true,
|
||||
title: t`First Name`
|
||||
},
|
||||
{
|
||||
accessor: 'last_name',
|
||||
sortable: true,
|
||||
title: t`Last Name`
|
||||
},
|
||||
{
|
||||
accessor: 'groups',
|
||||
sortable: true,
|
||||
switchable: true,
|
||||
title: t`Groups`,
|
||||
render: (record: any) => {
|
||||
return record.groups.length;
|
||||
}
|
||||
},
|
||||
BooleanColumn({
|
||||
accessor: 'is_staff',
|
||||
title: t`Staff`
|
||||
}),
|
||||
BooleanColumn({
|
||||
accessor: 'is_superuser',
|
||||
title: t`Superuser`
|
||||
}),
|
||||
BooleanColumn({
|
||||
accessor: 'is_active',
|
||||
title: t`Active`
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const rowActions = useCallback((record: UserDetailI): RowAction[] => {
|
||||
return [
|
||||
RowEditAction({
|
||||
onClick: () => {
|
||||
openEditApiForm({
|
||||
url: ApiPaths.user_list,
|
||||
pk: record.pk,
|
||||
title: t`Edit user`,
|
||||
fields: {
|
||||
email: {},
|
||||
first_name: {},
|
||||
last_name: {}
|
||||
},
|
||||
onFormSuccess: refreshTable,
|
||||
successMessage: t`User updated`
|
||||
});
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
onClick: () => {
|
||||
openDeleteApiForm({
|
||||
url: ApiPaths.user_list,
|
||||
pk: record.pk,
|
||||
title: t`Delete user`,
|
||||
successMessage: t`user deleted`,
|
||||
onFormSuccess: refreshTable,
|
||||
preFormContent: (
|
||||
<Text>{t`Are you sure you want to delete this user?`}</Text>
|
||||
)
|
||||
});
|
||||
}
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const addUser = useCallback(() => {
|
||||
openCreateApiForm({
|
||||
url: ApiPaths.user_list,
|
||||
title: t`Add user`,
|
||||
fields: {
|
||||
username: {},
|
||||
email: {},
|
||||
first_name: {},
|
||||
last_name: {}
|
||||
},
|
||||
onFormSuccess: refreshTable,
|
||||
successMessage: t`Added user`
|
||||
});
|
||||
}, []);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
let actions = [];
|
||||
|
||||
actions.push(
|
||||
<AddItemButton key="add-user" onClick={addUser} tooltip={t`Add user`} />
|
||||
);
|
||||
|
||||
return actions;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserDrawer
|
||||
opened={opened}
|
||||
close={close}
|
||||
refreshTable={refreshTable}
|
||||
userDetail={userDetail}
|
||||
/>
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiPaths.user_list)}
|
||||
tableKey={tableKey}
|
||||
columns={columns}
|
||||
props={{
|
||||
rowActions: rowActions,
|
||||
customActionGroups: tableActions,
|
||||
onRowClick: (record: any) => {
|
||||
setUserDetail(record);
|
||||
open();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -57,6 +57,11 @@ export const menuItems: MenuLinkItem[] = [
|
||||
id: 'settings-system',
|
||||
text: <Trans>System Settings</Trans>,
|
||||
link: '/settings/system'
|
||||
},
|
||||
{
|
||||
id: 'settings-admin',
|
||||
text: <Trans>Admin Center</Trans>,
|
||||
link: '/settings/admin'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -21,6 +21,7 @@ export enum ApiPaths {
|
||||
user_email_remove = 'api-user-email-remove',
|
||||
|
||||
user_list = 'api-user-list',
|
||||
group_list = 'api-group-list',
|
||||
owner_list = 'api-owner-list',
|
||||
|
||||
settings_global_list = 'api-settings-global-list',
|
||||
|
@ -16,6 +16,8 @@ import { PlaceholderPill } from '../../../components/items/Placeholder';
|
||||
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
|
||||
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
||||
import { GlobalSettingList } from '../../../components/settings/SettingList';
|
||||
import { GroupTable } from '../../../components/tables/settings/GroupTable';
|
||||
import { UserTable } from '../../../components/tables/settings/UserTable';
|
||||
|
||||
/**
|
||||
* System settings page
|
||||
@ -28,7 +30,14 @@ export default function AdminCenter() {
|
||||
label: t`User Management`,
|
||||
content: (
|
||||
<Stack spacing="xs">
|
||||
<PlaceholderPill />
|
||||
<Title order={5}>
|
||||
<Trans>Users</Trans>
|
||||
</Title>
|
||||
<UserTable />
|
||||
<Title order={5}>
|
||||
<Trans>Groups</Trans>
|
||||
</Title>
|
||||
<GroupTable />
|
||||
<Divider />
|
||||
<Stack spacing={0}>
|
||||
<Text>
|
||||
@ -87,7 +96,7 @@ export default function AdminCenter() {
|
||||
<Stack spacing="xs">
|
||||
<SettingsHeader
|
||||
title={t`Admin Center`}
|
||||
subtitle={t`Advanced Amininistrative Options for InvenTree`}
|
||||
subtitle={t`Advanced Options`}
|
||||
switch_link="/settings/system"
|
||||
switch_text="System Settings"
|
||||
/>
|
||||
|
@ -116,6 +116,12 @@ export function apiEndpoint(path: ApiPaths): string {
|
||||
return 'version/';
|
||||
case ApiPaths.sso_providers:
|
||||
return 'auth/providers/';
|
||||
case ApiPaths.user_list:
|
||||
return 'user/';
|
||||
case ApiPaths.group_list:
|
||||
return 'user/group/';
|
||||
case ApiPaths.owner_list:
|
||||
return 'user/owner/';
|
||||
case ApiPaths.build_order_list:
|
||||
return 'build/';
|
||||
case ApiPaths.build_order_attachment_list:
|
||||
|
Loading…
Reference in New Issue
Block a user