mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
[PUI] Settings simplification and restructure (#5822)
* unify file structure for settings * fixed router * clean up menu * refactored user settings mergerd profile and user setttings * removed profile page entirely * cleaned up account panels * use more detailed link * refactored settings page header * fixed user settings save * simplified user data handling * fixed UserState invalidation after form submition * removed username from account change this can currently not be done safely * Added basic security section * Added way to remove SSO account * Only show providers that are not in use * Changed API to contain configuration change * removed unused var * Added email section to PUI * Switched rending to vertical * Added things for adding a new email * removed sessions as we are not using that in PUI * made rendering logic easier to understand * alligned colums horizontally * allign action buttons for email
This commit is contained in:
parent
7b9c618658
commit
92336f6b32
@ -7,6 +7,10 @@ INVENTREE_API_VERSION = 145
|
|||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v146 -> 2023-11-02: https://github.com/inventree/InvenTree/pull/5822
|
||||||
|
- Extended SSO Provider endpoint to contain if a provider is configured
|
||||||
|
- Adds API endpoints for Email Address model
|
||||||
|
|
||||||
v145 -> 2023-10-30: https://github.com/inventree/InvenTree/pull/5786
|
v145 -> 2023-10-30: https://github.com/inventree/InvenTree/pull/5786
|
||||||
- Allow printing labels via POST including printing options in the body
|
- Allow printing labels via POST including printing options in the body
|
||||||
|
|
||||||
|
@ -4,17 +4,21 @@ from importlib import import_module
|
|||||||
|
|
||||||
from django.urls import include, path, reverse
|
from django.urls import include, path, reverse
|
||||||
|
|
||||||
|
from allauth.account.models import EmailAddress
|
||||||
from allauth.socialaccount import providers
|
from allauth.socialaccount import providers
|
||||||
from allauth.socialaccount.models import SocialApp
|
from allauth.socialaccount.models import SocialApp
|
||||||
from allauth.socialaccount.providers.keycloak.views import \
|
from allauth.socialaccount.providers.keycloak.views import \
|
||||||
KeycloakOAuth2Adapter
|
KeycloakOAuth2Adapter
|
||||||
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
|
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
|
||||||
OAuth2LoginView)
|
OAuth2LoginView)
|
||||||
from rest_framework.generics import ListAPIView
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.exceptions import NotFound
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI
|
||||||
|
from InvenTree.serializers import InvenTreeModelSerializer
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
@ -96,7 +100,7 @@ for provider in providers.registry.get_list():
|
|||||||
social_auth_urlpatterns += provider_urlpatterns
|
social_auth_urlpatterns += provider_urlpatterns
|
||||||
|
|
||||||
|
|
||||||
class SocialProviderListView(ListAPIView):
|
class SocialProviderListView(ListAPI):
|
||||||
"""List of available social providers."""
|
"""List of available social providers."""
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
@ -109,9 +113,12 @@ class SocialProviderListView(ListAPIView):
|
|||||||
'name': provider.name,
|
'name': provider.name,
|
||||||
'login': request.build_absolute_uri(reverse(f'{provider.id}_api_login')),
|
'login': request.build_absolute_uri(reverse(f'{provider.id}_api_login')),
|
||||||
'connect': request.build_absolute_uri(reverse(f'{provider.id}_api_connect')),
|
'connect': request.build_absolute_uri(reverse(f'{provider.id}_api_connect')),
|
||||||
|
'configured': False
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
provider_data['display_name'] = provider.get_app(request).name
|
provider_app = provider.get_app(request)
|
||||||
|
provider_data['display_name'] = provider_app.name
|
||||||
|
provider_data['configured'] = True
|
||||||
except SocialApp.DoesNotExist:
|
except SocialApp.DoesNotExist:
|
||||||
provider_data['display_name'] = provider.name
|
provider_data['display_name'] = provider.name
|
||||||
|
|
||||||
@ -124,3 +131,81 @@ class SocialProviderListView(ListAPIView):
|
|||||||
'providers': provider_list
|
'providers': provider_list
|
||||||
}
|
}
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailAddressSerializer(InvenTreeModelSerializer):
|
||||||
|
"""Serializer for the EmailAddress model."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta options for EmailAddressSerializer."""
|
||||||
|
|
||||||
|
model = EmailAddress
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyEmailAddressSerializer(InvenTreeModelSerializer):
|
||||||
|
"""Empty Serializer for the EmailAddress model."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta options for EmailAddressSerializer."""
|
||||||
|
|
||||||
|
model = EmailAddress
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
|
||||||
|
class EmailListView(ListCreateAPI):
|
||||||
|
"""List of registered email addresses for current users."""
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
serializer_class = EmailAddressSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Only return data for current user."""
|
||||||
|
return EmailAddress.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailActionMixin(CreateAPI):
|
||||||
|
"""Mixin to modify email addresses for current users."""
|
||||||
|
serializer_class = EmptyEmailAddressSerializer
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Filter queryset for current user."""
|
||||||
|
return EmailAddress.objects.filter(user=self.request.user, pk=self.kwargs['pk']).first()
|
||||||
|
|
||||||
|
@extend_schema(responses={200: OpenApiResponse(response=EmailAddressSerializer)})
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""Filter item, run action and return data."""
|
||||||
|
email = self.get_queryset()
|
||||||
|
if not email:
|
||||||
|
raise NotFound
|
||||||
|
|
||||||
|
self.special_action(email, request, *args, **kwargs)
|
||||||
|
return Response(EmailAddressSerializer(email).data)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailVerifyView(EmailActionMixin):
|
||||||
|
"""Re-verify an email for a currently logged in user."""
|
||||||
|
|
||||||
|
def special_action(self, email, request, *args, **kwargs):
|
||||||
|
"""Send confirmation."""
|
||||||
|
if email.verified:
|
||||||
|
return
|
||||||
|
email.send_confirmation(request)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailPrimaryView(EmailActionMixin):
|
||||||
|
"""Make an email for a currently logged in user primary."""
|
||||||
|
|
||||||
|
def special_action(self, email, *args, **kwargs):
|
||||||
|
"""Mark email as primary."""
|
||||||
|
if email.primary:
|
||||||
|
return
|
||||||
|
email.set_as_primary()
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRemoveView(EmailActionMixin):
|
||||||
|
"""Remove an email for a currently logged in user."""
|
||||||
|
|
||||||
|
def special_action(self, email, *args, **kwargs):
|
||||||
|
"""Delete email."""
|
||||||
|
email.delete()
|
||||||
|
@ -38,7 +38,9 @@ from web.urls import urlpatterns as platform_urls
|
|||||||
|
|
||||||
from .api import APISearchView, InfoView, NotFoundView, VersionView
|
from .api import APISearchView, InfoView, NotFoundView, VersionView
|
||||||
from .magic_login import GetSimpleLoginView
|
from .magic_login import GetSimpleLoginView
|
||||||
from .social_auth_urls import SocialProviderListView, social_auth_urlpatterns
|
from .social_auth_urls import (EmailListView, EmailPrimaryView,
|
||||||
|
EmailRemoveView, EmailVerifyView,
|
||||||
|
SocialProviderListView, social_auth_urlpatterns)
|
||||||
from .views import (AboutView, AppearanceSelectView, CustomConnectionsView,
|
from .views import (AboutView, AppearanceSelectView, CustomConnectionsView,
|
||||||
CustomEmailView, CustomLoginView,
|
CustomEmailView, CustomLoginView,
|
||||||
CustomPasswordResetFromKeyView,
|
CustomPasswordResetFromKeyView,
|
||||||
@ -85,6 +87,12 @@ apipatterns = [
|
|||||||
re_path(r'^registration/account-confirm-email/(?P<key>[-:\w]+)/$', ConfirmEmailView.as_view(), name='account_confirm_email'),
|
re_path(r'^registration/account-confirm-email/(?P<key>[-:\w]+)/$', ConfirmEmailView.as_view(), name='account_confirm_email'),
|
||||||
path('registration/', include('dj_rest_auth.registration.urls')),
|
path('registration/', include('dj_rest_auth.registration.urls')),
|
||||||
path('providers/', SocialProviderListView.as_view(), name='social_providers'),
|
path('providers/', SocialProviderListView.as_view(), name='social_providers'),
|
||||||
|
path('emails/', include([path('<int:pk>/', include([
|
||||||
|
path('primary/', EmailPrimaryView.as_view(), name='email-primary'),
|
||||||
|
path('verify/', EmailVerifyView.as_view(), name='email-verify'),
|
||||||
|
path('remove/', EmailRemoveView().as_view(), name='email-remove'),])),
|
||||||
|
path('', EmailListView.as_view(), name='email-list')
|
||||||
|
])),
|
||||||
path('social/', include(social_auth_urlpatterns)),
|
path('social/', include(social_auth_urlpatterns)),
|
||||||
path('social/', SocialAccountListView.as_view(), name='social_account_list'),
|
path('social/', SocialAccountListView.as_view(), name='social_account_list'),
|
||||||
path('social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
|
path('social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
IconLogout,
|
IconLogout,
|
||||||
IconPlugConnected,
|
IconPlugConnected,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconUserCircle,
|
|
||||||
IconUserCog
|
IconUserCog
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -13,7 +12,6 @@ import { Link } from 'react-router-dom';
|
|||||||
import { doClassicLogout } from '../../functions/auth';
|
import { doClassicLogout } from '../../functions/auth';
|
||||||
import { InvenTreeStyle } from '../../globalStyle';
|
import { InvenTreeStyle } from '../../globalStyle';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { PlaceholderPill } from '../items/Placeholder';
|
|
||||||
|
|
||||||
export function MainMenu() {
|
export function MainMenu() {
|
||||||
const { classes, theme } = InvenTreeStyle();
|
const { classes, theme } = InvenTreeStyle();
|
||||||
@ -36,21 +34,18 @@ export function MainMenu() {
|
|||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item icon={<IconUserCircle />}>
|
|
||||||
<Trans>Profile</Trans> <PlaceholderPill />
|
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
<Menu.Label>
|
<Menu.Label>
|
||||||
<Trans>Settings</Trans>
|
<Trans>Settings</Trans>
|
||||||
</Menu.Label>
|
</Menu.Label>
|
||||||
<Menu.Item icon={<IconSettings />} component={Link} to="/profile/user">
|
|
||||||
<Trans>Account settings</Trans>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item icon={<IconUserCog />} component={Link} to="/settings/user">
|
<Menu.Item icon={<IconUserCog />} component={Link} to="/settings/user">
|
||||||
<Trans>Account settings</Trans>
|
<Trans>Account settings</Trans>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{userState.user?.is_staff && (
|
{userState.user?.is_staff && (
|
||||||
<Menu.Item icon={<IconSettings />} component={Link} to="/settings/">
|
<Menu.Item
|
||||||
|
icon={<IconSettings />}
|
||||||
|
component={Link}
|
||||||
|
to="/settings/system"
|
||||||
|
>
|
||||||
<Trans>System Settings</Trans>
|
<Trans>System Settings</Trans>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
|
41
src/frontend/src/components/nav/SettingsHeader.tsx
Normal file
41
src/frontend/src/components/nav/SettingsHeader.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a settings page header with interlinks to one other settings page
|
||||||
|
*/
|
||||||
|
export function SettingsHeader({
|
||||||
|
title,
|
||||||
|
shorthand,
|
||||||
|
subtitle,
|
||||||
|
switch_condition = true,
|
||||||
|
switch_text,
|
||||||
|
switch_link
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
shorthand?: string;
|
||||||
|
subtitle: string | ReactNode;
|
||||||
|
switch_condition?: boolean;
|
||||||
|
switch_text: string | ReactNode;
|
||||||
|
switch_link: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<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_condition && (
|
||||||
|
<Anchor component={Link} to={switch_link}>
|
||||||
|
<IconSwitch size={14} />
|
||||||
|
{switch_text}
|
||||||
|
</Anchor>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@ -1,33 +0,0 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
|
||||||
import { Tabs } from '@mantine/core';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { StylishText } from '../../../components/items/StylishText';
|
|
||||||
import { UserPanel } from './UserPanel';
|
|
||||||
|
|
||||||
export default function Profile() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { tabValue } = useParams();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StylishText>
|
|
||||||
<Trans>Profile</Trans>
|
|
||||||
</StylishText>
|
|
||||||
<Tabs
|
|
||||||
value={tabValue}
|
|
||||||
onTabChange={(value) => navigate(`/profile/${value}`)}
|
|
||||||
>
|
|
||||||
<Tabs.List>
|
|
||||||
<Tabs.Tab value="user">
|
|
||||||
<Trans>User</Trans>
|
|
||||||
</Tabs.Tab>
|
|
||||||
</Tabs.List>
|
|
||||||
|
|
||||||
<Tabs.Panel value="user">
|
|
||||||
<UserPanel />
|
|
||||||
</Tabs.Panel>
|
|
||||||
</Tabs>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,160 +0,0 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Grid,
|
|
||||||
Group,
|
|
||||||
SimpleGrid,
|
|
||||||
Skeleton,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Title
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useForm } from '@mantine/form';
|
|
||||||
import { useToggle } from '@mantine/hooks';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { api, queryClient } from '../../../App';
|
|
||||||
import { ColorToggle } from '../../../components/items/ColorToggle';
|
|
||||||
import { EditButton } from '../../../components/items/EditButton';
|
|
||||||
import { LanguageSelect } from '../../../components/items/LanguageSelect';
|
|
||||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
|
||||||
import { useLocalState } from '../../../states/LocalState';
|
|
||||||
import { UserTheme } from './UserTheme';
|
|
||||||
|
|
||||||
export function UserPanel() {
|
|
||||||
// view
|
|
||||||
const PRIMARY_COL_HEIGHT = 300;
|
|
||||||
const SECONDARY_COL_HEIGHT = PRIMARY_COL_HEIGHT / 2 - 8;
|
|
||||||
|
|
||||||
// data
|
|
||||||
function fetchData() {
|
|
||||||
// TODO: Replace this call with the global user state, perhaps?
|
|
||||||
return api.get(apiUrl(ApiPaths.user_me)).then((res) => res.data);
|
|
||||||
}
|
|
||||||
const { isLoading, data } = useQuery({
|
|
||||||
queryKey: ['user-me'],
|
|
||||||
queryFn: fetchData
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SimpleGrid cols={2} spacing="md">
|
|
||||||
<Container w="100%">
|
|
||||||
{isLoading ? (
|
|
||||||
<Skeleton height={SECONDARY_COL_HEIGHT} />
|
|
||||||
) : (
|
|
||||||
<UserInfo data={data} />
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
<Grid gutter="md">
|
|
||||||
<Grid.Col>
|
|
||||||
<UserTheme height={SECONDARY_COL_HEIGHT} />
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<DisplaySettings height={SECONDARY_COL_HEIGHT} />
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<Skeleton height={SECONDARY_COL_HEIGHT} />
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</SimpleGrid>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UserInfo({ data }: { data: any }) {
|
|
||||||
if (!data) return <Skeleton />;
|
|
||||||
|
|
||||||
const form = useForm({ initialValues: data });
|
|
||||||
const [editing, setEditing] = useToggle([false, true] as const);
|
|
||||||
function SaveData(values: any) {
|
|
||||||
api.put(apiUrl(ApiPaths.user_me)).then((res) => {
|
|
||||||
if (res.status === 200) {
|
|
||||||
setEditing();
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['user-me'] });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={form.onSubmit((values) => SaveData(values))}>
|
|
||||||
<Group>
|
|
||||||
<Title order={3}>
|
|
||||||
<Trans>Userinfo</Trans>
|
|
||||||
</Title>
|
|
||||||
<EditButton setEditing={setEditing} editing={editing} />
|
|
||||||
</Group>
|
|
||||||
<Group>
|
|
||||||
{editing ? (
|
|
||||||
<Stack spacing="xs">
|
|
||||||
<TextInput
|
|
||||||
label="First name"
|
|
||||||
placeholder="First name"
|
|
||||||
{...form.getInputProps('first_name')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Last name"
|
|
||||||
placeholder="Last name"
|
|
||||||
{...form.getInputProps('last_name')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Username"
|
|
||||||
placeholder="Username"
|
|
||||||
{...form.getInputProps('username')}
|
|
||||||
/>
|
|
||||||
<Group position="right" mt="md">
|
|
||||||
<Button type="submit">
|
|
||||||
<Trans>Submit</Trans>
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
<Stack spacing="xs">
|
|
||||||
<Text>
|
|
||||||
<Trans>First name: {form.values.first_name}</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<Trans>Last name: {form.values.last_name}</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<Trans>Username: {form.values.username}</Trans>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DisplaySettings({ height }: { height: number }) {
|
|
||||||
function enablePseudoLang(): void {
|
|
||||||
useLocalState.setState({ language: 'pseudo-LOCALE' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container w="100%" mih={height} p={0}>
|
|
||||||
<Title order={3}>
|
|
||||||
<Trans>Display Settings</Trans>
|
|
||||||
</Title>
|
|
||||||
<Group>
|
|
||||||
<Text>
|
|
||||||
<Trans>Color Mode</Trans>
|
|
||||||
</Text>
|
|
||||||
<ColorToggle />
|
|
||||||
</Group>
|
|
||||||
<Group align="top">
|
|
||||||
<Text>
|
|
||||||
<Trans>Language</Trans>
|
|
||||||
</Text>
|
|
||||||
<Stack>
|
|
||||||
<LanguageSelect width={200} />
|
|
||||||
<Button onClick={enablePseudoLang} variant="light">
|
|
||||||
<Trans>Use pseudo language</Trans>
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,67 @@
|
|||||||
|
import { Trans } from '@lingui/macro';
|
||||||
|
import { Button, Group, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { useToggle } from '@mantine/hooks';
|
||||||
|
|
||||||
|
import { api } from '../../../../App';
|
||||||
|
import { EditButton } from '../../../../components/items/EditButton';
|
||||||
|
import { ApiPaths, apiUrl } from '../../../../states/ApiState';
|
||||||
|
import { useUserState } from '../../../../states/UserState';
|
||||||
|
|
||||||
|
export function AccountDetailPanel() {
|
||||||
|
const [user, fetchUserState] = useUserState((state) => [
|
||||||
|
state.user,
|
||||||
|
state.fetchUserState
|
||||||
|
]);
|
||||||
|
const form = useForm({ initialValues: user });
|
||||||
|
const [editing, setEditing] = useToggle([false, true] as const);
|
||||||
|
function SaveData(values: any) {
|
||||||
|
api.put(apiUrl(ApiPaths.user_me), values).then((res) => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
setEditing();
|
||||||
|
fetchUserState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={form.onSubmit((values) => SaveData(values))}>
|
||||||
|
<Group>
|
||||||
|
<Title order={3}>
|
||||||
|
<Trans>Account Details</Trans>
|
||||||
|
</Title>
|
||||||
|
<EditButton setEditing={setEditing} editing={editing} />
|
||||||
|
</Group>
|
||||||
|
<Group>
|
||||||
|
{editing ? (
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<TextInput
|
||||||
|
label="First name"
|
||||||
|
placeholder="First name"
|
||||||
|
{...form.getInputProps('first_name')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Last name"
|
||||||
|
placeholder="Last name"
|
||||||
|
{...form.getInputProps('last_name')}
|
||||||
|
/>
|
||||||
|
<Group position="right" mt="md">
|
||||||
|
<Button type="submit">
|
||||||
|
<Trans>Submit</Trans>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Stack spacing="0">
|
||||||
|
<Text>
|
||||||
|
<Trans>First name: {form.values.first_name}</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
<Trans>Last name: {form.values.last_name}</Trans>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import { Trans } from '@lingui/macro';
|
||||||
|
import { Button, Container, Group, Table, Title } from '@mantine/core';
|
||||||
|
|
||||||
|
import { ColorToggle } from '../../../../components/items/ColorToggle';
|
||||||
|
import { LanguageSelect } from '../../../../components/items/LanguageSelect';
|
||||||
|
import { useLocalState } from '../../../../states/LocalState';
|
||||||
|
|
||||||
|
export function DisplaySettingsPanel({ height }: { height: number }) {
|
||||||
|
function enablePseudoLang(): void {
|
||||||
|
useLocalState.setState({ language: 'pseudo-LOCALE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container w="100%" mih={height} p={0}>
|
||||||
|
<Title order={3}>
|
||||||
|
<Trans>Display Settings</Trans>
|
||||||
|
</Title>
|
||||||
|
<Table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Trans>Color Mode</Trans>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Group>
|
||||||
|
<ColorToggle />
|
||||||
|
</Group>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Trans>Language</Trans>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{' '}
|
||||||
|
<Group>
|
||||||
|
<LanguageSelect width={200} />
|
||||||
|
<Button onClick={enablePseudoLang} variant="light">
|
||||||
|
<Trans>Use pseudo language</Trans>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,319 @@
|
|||||||
|
import { Trans, t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
Radio,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconAlertCircle, IconAt } from '@tabler/icons-react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { api, queryClient } from '../../../../App';
|
||||||
|
import { PlaceholderPill } from '../../../../components/items/Placeholder';
|
||||||
|
import { ApiPaths, apiUrl } from '../../../../states/ApiState';
|
||||||
|
|
||||||
|
export function SecurityContent() {
|
||||||
|
const [isSsoEnabled, setIsSsoEnabled] = useState<boolean>(false);
|
||||||
|
const [isMfaEnabled, setIsMfaEnabled] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { isLoading: isLoadingProvider, data: dataProvider } = useQuery({
|
||||||
|
queryKey: ['sso-providers'],
|
||||||
|
queryFn: () =>
|
||||||
|
api.get(apiUrl(ApiPaths.sso_providers)).then((res) => res.data)
|
||||||
|
});
|
||||||
|
|
||||||
|
// evaluate if security options are enabled
|
||||||
|
useEffect(() => {
|
||||||
|
if (dataProvider === undefined) return;
|
||||||
|
|
||||||
|
// check if SSO is enabled on the server
|
||||||
|
setIsSsoEnabled(dataProvider.sso_enabled || false);
|
||||||
|
// check if MFa is enabled
|
||||||
|
setIsMfaEnabled(dataProvider.mfa_required || false);
|
||||||
|
}, [dataProvider]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={5}>
|
||||||
|
<Trans>Email</Trans>
|
||||||
|
</Title>
|
||||||
|
<EmailContent />
|
||||||
|
<Title order={5}>
|
||||||
|
<Trans>Single Sign On Accounts</Trans>
|
||||||
|
</Title>
|
||||||
|
{isSsoEnabled ? (
|
||||||
|
<SsoContent dataProvider={dataProvider} />
|
||||||
|
) : (
|
||||||
|
<Alert
|
||||||
|
icon={<IconAlertCircle size="1rem" />}
|
||||||
|
title={t`Not enabled`}
|
||||||
|
color="yellow"
|
||||||
|
>
|
||||||
|
<Trans>Single Sign On is not enabled for this server </Trans>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<Title order={5}>
|
||||||
|
<Trans>Multifactor</Trans>
|
||||||
|
</Title>
|
||||||
|
{isLoadingProvider ? (
|
||||||
|
<Loader />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{isMfaEnabled ? (
|
||||||
|
<MfaContent />
|
||||||
|
) : (
|
||||||
|
<Alert
|
||||||
|
icon={<IconAlertCircle size="1rem" />}
|
||||||
|
title={t`Not enabled`}
|
||||||
|
color="yellow"
|
||||||
|
>
|
||||||
|
<Trans>
|
||||||
|
Multifactor authentication is not configured for your account{' '}
|
||||||
|
</Trans>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmailContent({}: {}) {
|
||||||
|
const [value, setValue] = useState<string>('');
|
||||||
|
const [newEmailValue, setNewEmailValue] = useState('');
|
||||||
|
const { isLoading, data, refetch } = useQuery({
|
||||||
|
queryKey: ['emails'],
|
||||||
|
queryFn: () => api.get(apiUrl(ApiPaths.user_emails)).then((res) => res.data)
|
||||||
|
});
|
||||||
|
|
||||||
|
function runServerAction(url: ApiPaths) {
|
||||||
|
api
|
||||||
|
.post(apiUrl(url).replace('$id', value), {})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch((res) => console.log(res.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEmail() {
|
||||||
|
api
|
||||||
|
.post(apiUrl(ApiPaths.user_emails), {
|
||||||
|
email: newEmailValue
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch((res) => console.log(res.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) return <Loader />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Radio.Group
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
name="email_accounts"
|
||||||
|
label={t`The following email addresses are associated with your account:`}
|
||||||
|
>
|
||||||
|
<Stack mt="xs">
|
||||||
|
{data.map((link: any) => (
|
||||||
|
<Radio
|
||||||
|
key={link.id}
|
||||||
|
value={String(link.id)}
|
||||||
|
label={
|
||||||
|
<Group position="apart">
|
||||||
|
{link.email}
|
||||||
|
{link.primary && (
|
||||||
|
<Badge color="blue">
|
||||||
|
<Trans>Primary</Trans>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{link.verified ? (
|
||||||
|
<Badge color="green">
|
||||||
|
<Trans>Verified</Trans>
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge color="yellow">
|
||||||
|
<Trans>Unverified</Trans>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Radio.Group>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Stack>
|
||||||
|
<Text>
|
||||||
|
<Trans>Add Email Address</Trans>
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
label={t`E-Mail`}
|
||||||
|
placeholder={t`E-Mail address`}
|
||||||
|
icon={<IconAt />}
|
||||||
|
value={newEmailValue}
|
||||||
|
onChange={(event) => setNewEmailValue(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Group>
|
||||||
|
<Button onClick={() => runServerAction(ApiPaths.user_email_primary)}>
|
||||||
|
<Trans>Make Primary</Trans>
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => runServerAction(ApiPaths.user_email_verify)}>
|
||||||
|
<Trans>Re-send Verification</Trans>
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => runServerAction(ApiPaths.user_email_remove)}>
|
||||||
|
<Trans>Remove</Trans>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Button onClick={addEmail}>
|
||||||
|
<Trans>Add Email</Trans>
|
||||||
|
</Button>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SsoContent({ dataProvider }: { dataProvider: any | undefined }) {
|
||||||
|
const [value, setValue] = useState<string>('');
|
||||||
|
const [currentProviders, setcurrentProviders] = useState<[]>();
|
||||||
|
const { isLoading, data } = useQuery({
|
||||||
|
queryKey: ['sso-list'],
|
||||||
|
queryFn: () => api.get(apiUrl(ApiPaths.user_sso)).then((res) => res.data)
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dataProvider === undefined) return;
|
||||||
|
if (data === undefined) return;
|
||||||
|
|
||||||
|
const configuredProviders = data.map((item: any) => {
|
||||||
|
return item.provider;
|
||||||
|
});
|
||||||
|
function isAlreadyInUse(value: any) {
|
||||||
|
return !configuredProviders.includes(value.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove providers that are used currently
|
||||||
|
let newData = dataProvider.providers;
|
||||||
|
newData = newData.filter(isAlreadyInUse);
|
||||||
|
setcurrentProviders(newData);
|
||||||
|
}, [dataProvider, data]);
|
||||||
|
|
||||||
|
function removeProvider() {
|
||||||
|
api
|
||||||
|
.post(apiUrl(ApiPaths.user_sso_remove).replace('$id', value))
|
||||||
|
.then(() => {
|
||||||
|
queryClient.removeQueries({
|
||||||
|
queryKey: ['sso-list']
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((res) => console.log(res.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* renderer */
|
||||||
|
if (isLoading) return <Loader />;
|
||||||
|
|
||||||
|
function ProviderButton({ provider }: { provider: any }) {
|
||||||
|
const button = (
|
||||||
|
<Button
|
||||||
|
key={provider.id}
|
||||||
|
component="a"
|
||||||
|
href={provider.connect}
|
||||||
|
variant="outline"
|
||||||
|
disabled={!provider.configured}
|
||||||
|
>
|
||||||
|
<Group position="apart">
|
||||||
|
{provider.display_name}
|
||||||
|
{provider.configured == false && <IconAlertCircle />}
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (provider.configured) return button;
|
||||||
|
return (
|
||||||
|
<Tooltip label={t`Provider has not been configured`}>{button}</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
{data.length == 0 ? (
|
||||||
|
<Alert
|
||||||
|
icon={<IconAlertCircle size="1rem" />}
|
||||||
|
title={t`Not configured`}
|
||||||
|
color="yellow"
|
||||||
|
>
|
||||||
|
<Trans>
|
||||||
|
There are no social network accounts connected to this account.{' '}
|
||||||
|
</Trans>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Stack>
|
||||||
|
<Radio.Group
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
name="sso_accounts"
|
||||||
|
label={t`You can sign in to your account using any of the following third party accounts`}
|
||||||
|
>
|
||||||
|
<Stack mt="xs">
|
||||||
|
{data.map((link: any) => (
|
||||||
|
<Radio
|
||||||
|
key={link.id}
|
||||||
|
value={String(link.id)}
|
||||||
|
label={link.provider}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Radio.Group>
|
||||||
|
<Button onClick={removeProvider}>
|
||||||
|
<Trans>Remove</Trans>
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Stack>
|
||||||
|
<Text>Add SSO Account</Text>
|
||||||
|
<Text>
|
||||||
|
{currentProviders === undefined ? (
|
||||||
|
<Trans>Loading</Trans>
|
||||||
|
) : (
|
||||||
|
<Stack spacing="xs">
|
||||||
|
{currentProviders.map((provider: any) => (
|
||||||
|
<ProviderButton key={provider.id} provider={provider} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MfaContent({}: {}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
MFA Details
|
||||||
|
<PlaceholderPill />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Container, Grid, SimpleGrid } from '@mantine/core';
|
||||||
|
|
||||||
|
import { AccountDetailPanel } from './AccountDetailPanel';
|
||||||
|
import { DisplaySettingsPanel } from './DisplaySettingsPanel';
|
||||||
|
import { UserTheme } from './UserThemePanel';
|
||||||
|
|
||||||
|
export function AccountContent() {
|
||||||
|
const PRIMARY_COL_HEIGHT = 300;
|
||||||
|
const SECONDARY_COL_HEIGHT = PRIMARY_COL_HEIGHT / 2 - 8;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SimpleGrid cols={2} spacing="md">
|
||||||
|
<Container w="100%">
|
||||||
|
<AccountDetailPanel />
|
||||||
|
</Container>
|
||||||
|
<Grid gutter="md">
|
||||||
|
<Grid.Col>
|
||||||
|
<UserTheme height={SECONDARY_COL_HEIGHT} />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col>
|
||||||
|
<DisplaySettingsPanel height={SECONDARY_COL_HEIGHT} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</SimpleGrid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -8,17 +8,15 @@ import {
|
|||||||
Loader,
|
Loader,
|
||||||
Select,
|
Select,
|
||||||
Slider,
|
Slider,
|
||||||
Space,
|
|
||||||
Table,
|
Table,
|
||||||
Title
|
Title
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { LoaderType } from '@mantine/styles/lib/theme/types/MantineTheme';
|
import { LoaderType } from '@mantine/styles/lib/theme/types/MantineTheme';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { PlaceholderPill } from '../../../components/items/Placeholder';
|
import { SizeMarks } from '../../../../defaults/defaults';
|
||||||
import { SizeMarks } from '../../../defaults/defaults';
|
import { InvenTreeStyle } from '../../../../globalStyle';
|
||||||
import { InvenTreeStyle } from '../../../globalStyle';
|
import { useLocalState } from '../../../../states/LocalState';
|
||||||
import { useLocalState } from '../../../states/LocalState';
|
|
||||||
|
|
||||||
function getLkp(color: string) {
|
function getLkp(color: string) {
|
||||||
return { [DEFAULT_THEME.colors[color][6]]: color };
|
return { [DEFAULT_THEME.colors[color][6]]: color };
|
||||||
@ -80,9 +78,7 @@ export function UserTheme({ height }: { height: number }) {
|
|||||||
return (
|
return (
|
||||||
<Container w="100%" mih={height} p={0}>
|
<Container w="100%" mih={height} p={0}>
|
||||||
<Title order={3}>
|
<Title order={3}>
|
||||||
<Trans>
|
<Trans>Theme</Trans>
|
||||||
Design <PlaceholderPill />
|
|
||||||
</Trans>
|
|
||||||
</Title>
|
</Title>
|
||||||
<Table>
|
<Table>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -137,13 +133,12 @@ export function UserTheme({ height }: { height: number }) {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Group align="center">
|
<Group align="center">
|
||||||
<Loader type={loader} mah={18} />
|
|
||||||
<Space w={10} />
|
|
||||||
<Select
|
<Select
|
||||||
data={loaderDate}
|
data={loaderDate}
|
||||||
value={loader}
|
value={loader}
|
||||||
onChange={changeLoader}
|
onChange={changeLoader}
|
||||||
/>
|
/>
|
||||||
|
<Loader type={loader} mah={18} />
|
||||||
</Group>
|
</Group>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
@ -3,11 +3,11 @@ import { LoadingOverlay, Stack } from '@mantine/core';
|
|||||||
import { IconPlugConnected } from '@tabler/icons-react';
|
import { IconPlugConnected } from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../../components/nav/PageDetail';
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
|
||||||
import { PluginListTable } from '../../components/tables/plugin/PluginListTable';
|
import { PluginListTable } from '../../../components/tables/plugin/PluginListTable';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../../hooks/UseInstance';
|
||||||
import { ApiPaths } from '../../states/ApiState';
|
import { ApiPaths } from '../../../states/ApiState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugins settings page
|
* Plugins settings page
|
@ -1,4 +1,4 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
import { Divider, Stack } from '@mantine/core';
|
import { Divider, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBellCog,
|
IconBellCog,
|
||||||
@ -21,11 +21,12 @@ import {
|
|||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
||||||
import { GlobalSettingList } from '../../components/settings/SettingList';
|
import { GlobalSettingList } from '../../../components/settings/SettingList';
|
||||||
import { CustomUnitsTable } from '../../components/tables/settings/CustomUnitsTable';
|
import { CustomUnitsTable } from '../../../components/tables/settings/CustomUnitsTable';
|
||||||
import { ProjectCodeTable } from '../../components/tables/settings/ProjectCodeTable';
|
import { ProjectCodeTable } from '../../../components/tables/settings/ProjectCodeTable';
|
||||||
|
import { useServerApiState } from '../../../states/ApiState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* System settings page
|
* System settings page
|
||||||
@ -249,11 +250,17 @@ export default function SystemSettings() {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
const [server] = useServerApiState((state) => [state.server]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<PageDetail title={t`System Settings`} />
|
<SettingsHeader
|
||||||
|
title={server.instance || ''}
|
||||||
|
subtitle={<Trans>System Settings</Trans>}
|
||||||
|
switch_link="/settings/user"
|
||||||
|
switch_text={<Trans>Switch to User Setting</Trans>}
|
||||||
|
/>
|
||||||
<PanelGroup pageKey="system-settings" panels={systemSettingsPanels} />
|
<PanelGroup pageKey="system-settings" panels={systemSettingsPanels} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
@ -1,18 +1,22 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
import { Stack, Text } from '@mantine/core';
|
import { Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBellCog,
|
IconBellCog,
|
||||||
IconDeviceDesktop,
|
IconDeviceDesktop,
|
||||||
IconDeviceDesktopAnalytics,
|
IconDeviceDesktopAnalytics,
|
||||||
IconFileAnalytics,
|
IconFileAnalytics,
|
||||||
|
IconLock,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconUserCircle
|
IconUserCircle
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
||||||
import { UserSettingList } from '../../components/settings/SettingList';
|
import { UserSettingList } from '../../../components/settings/SettingList';
|
||||||
|
import { useUserState } from '../../../states/UserState';
|
||||||
|
import { SecurityContent } from './AccountSettings/SecurityContent';
|
||||||
|
import { AccountContent } from './AccountSettings/UserPanel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User settings page
|
* User settings page
|
||||||
@ -23,7 +27,14 @@ export default function UserSettings() {
|
|||||||
{
|
{
|
||||||
name: 'account',
|
name: 'account',
|
||||||
label: t`Account`,
|
label: t`Account`,
|
||||||
icon: <IconUserCircle />
|
icon: <IconUserCircle />,
|
||||||
|
content: <AccountContent />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'security',
|
||||||
|
label: t`Security`,
|
||||||
|
icon: <IconLock />,
|
||||||
|
content: <SecurityContent />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
@ -95,13 +106,18 @@ export default function UserSettings() {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
const [user] = useUserState((state) => [state.user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<PageDetail
|
<SettingsHeader
|
||||||
title={t`User Settings`}
|
title={`${user?.first_name} ${user?.last_name}`}
|
||||||
detail={<Text>TODO: Filler</Text>}
|
shorthand={user?.username || ''}
|
||||||
|
subtitle={<Trans>Account Settings</Trans>}
|
||||||
|
switch_link="/settings/system"
|
||||||
|
switch_text={<Trans>Switch to System Setting</Trans>}
|
||||||
|
switch_condition={user?.is_staff || false}
|
||||||
/>
|
/>
|
||||||
<PanelGroup pageKey="user-settings" panels={userSettingsPanels} />
|
<PanelGroup pageKey="user-settings" panels={userSettingsPanels} />
|
||||||
</Stack>
|
</Stack>
|
@ -81,20 +81,16 @@ export const Notifications = Loadable(
|
|||||||
lazy(() => import('./pages/Notifications'))
|
lazy(() => import('./pages/Notifications'))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Profile = Loadable(
|
|
||||||
lazy(() => import('./pages/Index/Profile/Profile'))
|
|
||||||
);
|
|
||||||
|
|
||||||
export const UserSettings = Loadable(
|
export const UserSettings = Loadable(
|
||||||
lazy(() => import('./pages/Index/UserSettings'))
|
lazy(() => import('./pages/Index/Settings/UserSettings'))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SystemSettings = Loadable(
|
export const SystemSettings = Loadable(
|
||||||
lazy(() => import('./pages/Index/SystemSettings'))
|
lazy(() => import('./pages/Index/Settings/SystemSettings'))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PluginSettings = Loadable(
|
export const PluginSettings = Loadable(
|
||||||
lazy(() => import('./pages/Index/PluginSettings'))
|
lazy(() => import('./pages/Index/Settings/PluginSettings'))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const NotFound = Loadable(lazy(() => import('./pages/NotFound')));
|
export const NotFound = Loadable(lazy(() => import('./pages/NotFound')));
|
||||||
@ -118,6 +114,7 @@ export const routes = (
|
|||||||
<Route path="scan/" element={<Scan />} />,
|
<Route path="scan/" element={<Scan />} />,
|
||||||
<Route path="settings/">
|
<Route path="settings/">
|
||||||
<Route index element={<SystemSettings />} />
|
<Route index element={<SystemSettings />} />
|
||||||
|
<Route path="system/" element={<SystemSettings />} />
|
||||||
<Route path="user/" element={<UserSettings />} />
|
<Route path="user/" element={<UserSettings />} />
|
||||||
<Route path="plugin/" element={<PluginSettings />} />
|
<Route path="plugin/" element={<PluginSettings />} />
|
||||||
</Route>
|
</Route>
|
||||||
@ -148,7 +145,6 @@ export const routes = (
|
|||||||
<Route path="return-order/:id/" element={<ReturnOrderDetail />} />
|
<Route path="return-order/:id/" element={<ReturnOrderDetail />} />
|
||||||
<Route path="customer/:id/" element={<CustomerDetail />} />
|
<Route path="customer/:id/" element={<CustomerDetail />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/profile/:tabValue" element={<Profile />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/" errorElement={<ErrorPage />}>
|
<Route path="/" errorElement={<ErrorPage />}>
|
||||||
<Route path="/login" element={<Login />} />,
|
<Route path="/login" element={<Login />} />,
|
||||||
|
@ -60,6 +60,12 @@ export enum ApiPaths {
|
|||||||
user_simple_login = 'api-user-simple-login',
|
user_simple_login = 'api-user-simple-login',
|
||||||
user_reset = 'api-user-reset',
|
user_reset = 'api-user-reset',
|
||||||
user_reset_set = 'api-user-reset-set',
|
user_reset_set = 'api-user-reset-set',
|
||||||
|
user_sso = 'api-user-sso',
|
||||||
|
user_sso_remove = 'api-user-sso-remove',
|
||||||
|
user_emails = 'api-user-emails',
|
||||||
|
user_email_verify = 'api-user-email-verify',
|
||||||
|
user_email_primary = 'api-user-email-primary',
|
||||||
|
user_email_remove = 'api-user-email-remove',
|
||||||
|
|
||||||
settings_global_list = 'api-settings-global-list',
|
settings_global_list = 'api-settings-global-list',
|
||||||
settings_user_list = 'api-settings-user-list',
|
settings_user_list = 'api-settings-user-list',
|
||||||
@ -69,6 +75,7 @@ export enum ApiPaths {
|
|||||||
news = 'news',
|
news = 'news',
|
||||||
global_status = 'api-global-status',
|
global_status = 'api-global-status',
|
||||||
version = 'api-version',
|
version = 'api-version',
|
||||||
|
sso_providers = 'api-sso-providers',
|
||||||
|
|
||||||
// Build order URLs
|
// Build order URLs
|
||||||
build_order_list = 'api-build-list',
|
build_order_list = 'api-build-list',
|
||||||
@ -141,10 +148,22 @@ export function apiEndpoint(path: ApiPaths): string {
|
|||||||
return 'email/generate/';
|
return 'email/generate/';
|
||||||
case ApiPaths.user_reset:
|
case ApiPaths.user_reset:
|
||||||
// Note leading prefix here
|
// Note leading prefix here
|
||||||
return '/auth/password/reset/';
|
return 'auth/password/reset/';
|
||||||
case ApiPaths.user_reset_set:
|
case ApiPaths.user_reset_set:
|
||||||
// Note leading prefix here
|
// Note leading prefix here
|
||||||
return '/auth/password/reset/confirm/';
|
return 'auth/password/reset/confirm/';
|
||||||
|
case ApiPaths.user_sso:
|
||||||
|
return 'auth/social/';
|
||||||
|
case ApiPaths.user_sso_remove:
|
||||||
|
return 'auth/social/$id/disconnect/';
|
||||||
|
case ApiPaths.user_emails:
|
||||||
|
return 'auth/emails/';
|
||||||
|
case ApiPaths.user_email_remove:
|
||||||
|
return 'auth/emails/$id/remove/';
|
||||||
|
case ApiPaths.user_email_verify:
|
||||||
|
return 'auth/emails/$id/verify/';
|
||||||
|
case ApiPaths.user_email_primary:
|
||||||
|
return 'auth/emails/$id/primary/';
|
||||||
case ApiPaths.api_search:
|
case ApiPaths.api_search:
|
||||||
return 'search/';
|
return 'search/';
|
||||||
case ApiPaths.settings_global_list:
|
case ApiPaths.settings_global_list:
|
||||||
@ -161,6 +180,8 @@ export function apiEndpoint(path: ApiPaths): string {
|
|||||||
return 'generic/status/';
|
return 'generic/status/';
|
||||||
case ApiPaths.version:
|
case ApiPaths.version:
|
||||||
return 'version/';
|
return 'version/';
|
||||||
|
case ApiPaths.sso_providers:
|
||||||
|
return 'auth/providers/';
|
||||||
case ApiPaths.build_order_list:
|
case ApiPaths.build_order_list:
|
||||||
return 'build/';
|
return 'build/';
|
||||||
case ApiPaths.build_order_attachment_list:
|
case ApiPaths.build_order_attachment_list:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user