mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 03:55:41 +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:
@ -5,7 +5,6 @@ import {
|
||||
IconLogout,
|
||||
IconPlugConnected,
|
||||
IconSettings,
|
||||
IconUserCircle,
|
||||
IconUserCog
|
||||
} from '@tabler/icons-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
@ -13,7 +12,6 @@ import { Link } from 'react-router-dom';
|
||||
import { doClassicLogout } from '../../functions/auth';
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { PlaceholderPill } from '../items/Placeholder';
|
||||
|
||||
export function MainMenu() {
|
||||
const { classes, theme } = InvenTreeStyle();
|
||||
@ -36,21 +34,18 @@ export function MainMenu() {
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item icon={<IconUserCircle />}>
|
||||
<Trans>Profile</Trans> <PlaceholderPill />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Label>
|
||||
<Trans>Settings</Trans>
|
||||
</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">
|
||||
<Trans>Account settings</Trans>
|
||||
</Menu.Item>
|
||||
{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>
|
||||
</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,
|
||||
Select,
|
||||
Slider,
|
||||
Space,
|
||||
Table,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { LoaderType } from '@mantine/styles/lib/theme/types/MantineTheme';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { PlaceholderPill } from '../../../components/items/Placeholder';
|
||||
import { SizeMarks } from '../../../defaults/defaults';
|
||||
import { InvenTreeStyle } from '../../../globalStyle';
|
||||
import { useLocalState } from '../../../states/LocalState';
|
||||
import { SizeMarks } from '../../../../defaults/defaults';
|
||||
import { InvenTreeStyle } from '../../../../globalStyle';
|
||||
import { useLocalState } from '../../../../states/LocalState';
|
||||
|
||||
function getLkp(color: string) {
|
||||
return { [DEFAULT_THEME.colors[color][6]]: color };
|
||||
@ -80,9 +78,7 @@ export function UserTheme({ height }: { height: number }) {
|
||||
return (
|
||||
<Container w="100%" mih={height} p={0}>
|
||||
<Title order={3}>
|
||||
<Trans>
|
||||
Design <PlaceholderPill />
|
||||
</Trans>
|
||||
<Trans>Theme</Trans>
|
||||
</Title>
|
||||
<Table>
|
||||
<tbody>
|
||||
@ -137,13 +133,12 @@ export function UserTheme({ height }: { height: number }) {
|
||||
</td>
|
||||
<td>
|
||||
<Group align="center">
|
||||
<Loader type={loader} mah={18} />
|
||||
<Space w={10} />
|
||||
<Select
|
||||
data={loaderDate}
|
||||
value={loader}
|
||||
onChange={changeLoader}
|
||||
/>
|
||||
<Loader type={loader} mah={18} />
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
@ -3,11 +3,11 @@ import { LoadingOverlay, Stack } from '@mantine/core';
|
||||
import { IconPlugConnected } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { PluginListTable } from '../../components/tables/plugin/PluginListTable';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { ApiPaths } from '../../states/ApiState';
|
||||
import { PageDetail } from '../../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
|
||||
import { PluginListTable } from '../../../components/tables/plugin/PluginListTable';
|
||||
import { useInstance } from '../../../hooks/UseInstance';
|
||||
import { ApiPaths } from '../../../states/ApiState';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
IconBellCog,
|
||||
@ -21,11 +21,12 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { GlobalSettingList } from '../../components/settings/SettingList';
|
||||
import { CustomUnitsTable } from '../../components/tables/settings/CustomUnitsTable';
|
||||
import { ProjectCodeTable } from '../../components/tables/settings/ProjectCodeTable';
|
||||
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
|
||||
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
||||
import { GlobalSettingList } from '../../../components/settings/SettingList';
|
||||
import { CustomUnitsTable } from '../../../components/tables/settings/CustomUnitsTable';
|
||||
import { ProjectCodeTable } from '../../../components/tables/settings/ProjectCodeTable';
|
||||
import { useServerApiState } from '../../../states/ApiState';
|
||||
|
||||
/**
|
||||
* System settings page
|
||||
@ -249,11 +250,17 @@ export default function SystemSettings() {
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
const [server] = useServerApiState((state) => [state.server]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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} />
|
||||
</Stack>
|
||||
</>
|
@ -1,18 +1,22 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Stack, Text } from '@mantine/core';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Stack } from '@mantine/core';
|
||||
import {
|
||||
IconBellCog,
|
||||
IconDeviceDesktop,
|
||||
IconDeviceDesktopAnalytics,
|
||||
IconFileAnalytics,
|
||||
IconLock,
|
||||
IconSearch,
|
||||
IconUserCircle
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { UserSettingList } from '../../components/settings/SettingList';
|
||||
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
|
||||
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
||||
import { UserSettingList } from '../../../components/settings/SettingList';
|
||||
import { useUserState } from '../../../states/UserState';
|
||||
import { SecurityContent } from './AccountSettings/SecurityContent';
|
||||
import { AccountContent } from './AccountSettings/UserPanel';
|
||||
|
||||
/**
|
||||
* User settings page
|
||||
@ -23,7 +27,14 @@ export default function UserSettings() {
|
||||
{
|
||||
name: 'account',
|
||||
label: t`Account`,
|
||||
icon: <IconUserCircle />
|
||||
icon: <IconUserCircle />,
|
||||
content: <AccountContent />
|
||||
},
|
||||
{
|
||||
name: 'security',
|
||||
label: t`Security`,
|
||||
icon: <IconLock />,
|
||||
content: <SecurityContent />
|
||||
},
|
||||
{
|
||||
name: 'dashboard',
|
||||
@ -95,13 +106,18 @@ export default function UserSettings() {
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
const [user] = useUserState((state) => [state.user]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<PageDetail
|
||||
title={t`User Settings`}
|
||||
detail={<Text>TODO: Filler</Text>}
|
||||
<SettingsHeader
|
||||
title={`${user?.first_name} ${user?.last_name}`}
|
||||
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} />
|
||||
</Stack>
|
@ -81,20 +81,16 @@ export const Notifications = Loadable(
|
||||
lazy(() => import('./pages/Notifications'))
|
||||
);
|
||||
|
||||
export const Profile = Loadable(
|
||||
lazy(() => import('./pages/Index/Profile/Profile'))
|
||||
);
|
||||
|
||||
export const UserSettings = Loadable(
|
||||
lazy(() => import('./pages/Index/UserSettings'))
|
||||
lazy(() => import('./pages/Index/Settings/UserSettings'))
|
||||
);
|
||||
|
||||
export const SystemSettings = Loadable(
|
||||
lazy(() => import('./pages/Index/SystemSettings'))
|
||||
lazy(() => import('./pages/Index/Settings/SystemSettings'))
|
||||
);
|
||||
|
||||
export const PluginSettings = Loadable(
|
||||
lazy(() => import('./pages/Index/PluginSettings'))
|
||||
lazy(() => import('./pages/Index/Settings/PluginSettings'))
|
||||
);
|
||||
|
||||
export const NotFound = Loadable(lazy(() => import('./pages/NotFound')));
|
||||
@ -118,6 +114,7 @@ export const routes = (
|
||||
<Route path="scan/" element={<Scan />} />,
|
||||
<Route path="settings/">
|
||||
<Route index element={<SystemSettings />} />
|
||||
<Route path="system/" element={<SystemSettings />} />
|
||||
<Route path="user/" element={<UserSettings />} />
|
||||
<Route path="plugin/" element={<PluginSettings />} />
|
||||
</Route>
|
||||
@ -148,7 +145,6 @@ export const routes = (
|
||||
<Route path="return-order/:id/" element={<ReturnOrderDetail />} />
|
||||
<Route path="customer/:id/" element={<CustomerDetail />} />
|
||||
</Route>
|
||||
<Route path="/profile/:tabValue" element={<Profile />} />
|
||||
</Route>
|
||||
<Route path="/" errorElement={<ErrorPage />}>
|
||||
<Route path="/login" element={<Login />} />,
|
||||
|
@ -60,6 +60,12 @@ export enum ApiPaths {
|
||||
user_simple_login = 'api-user-simple-login',
|
||||
user_reset = 'api-user-reset',
|
||||
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_user_list = 'api-settings-user-list',
|
||||
@ -69,6 +75,7 @@ export enum ApiPaths {
|
||||
news = 'news',
|
||||
global_status = 'api-global-status',
|
||||
version = 'api-version',
|
||||
sso_providers = 'api-sso-providers',
|
||||
|
||||
// Build order URLs
|
||||
build_order_list = 'api-build-list',
|
||||
@ -141,10 +148,22 @@ export function apiEndpoint(path: ApiPaths): string {
|
||||
return 'email/generate/';
|
||||
case ApiPaths.user_reset:
|
||||
// Note leading prefix here
|
||||
return '/auth/password/reset/';
|
||||
return 'auth/password/reset/';
|
||||
case ApiPaths.user_reset_set:
|
||||
// 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:
|
||||
return 'search/';
|
||||
case ApiPaths.settings_global_list:
|
||||
@ -161,6 +180,8 @@ export function apiEndpoint(path: ApiPaths): string {
|
||||
return 'generic/status/';
|
||||
case ApiPaths.version:
|
||||
return 'version/';
|
||||
case ApiPaths.sso_providers:
|
||||
return 'auth/providers/';
|
||||
case ApiPaths.build_order_list:
|
||||
return 'build/';
|
||||
case ApiPaths.build_order_attachment_list:
|
||||
|
Reference in New Issue
Block a user