P-UI: Fast language / theme / server selection (#5301)

* added language toggle component

* added language and theme controls onto start page

* moved host selection out of auth

* optimized rendering

* make server option less obvious

* changed EditButton save symbol

* longer welcome text

* removed ColorToggle color schema

* reduced code

* disabled host selection when options are changing

* fix type error

* use GH reporter

* fix tests?

* compile frontend

* fix assertation

* revert unneeded change

* split up into more components

* separated functions / use cases for LanguageToggle more

* moved color toggle to profile

* moved language out of main menu into profile

* remapped settings link
This commit is contained in:
Matthias Mair 2023-07-22 14:19:19 +02:00 committed by GitHub
parent f70294b247
commit f227315ad1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 226 additions and 151 deletions

View File

@ -414,7 +414,7 @@ jobs:
- name: Set up test data
run: invoke setup-test -i
- name: Install dependencies
run: cd src/frontend && yarn install
run: inv frontend-compile
- name: Install Playwright Browsers
run: cd src/frontend && npx playwright install --with-deps
- name: Run Playwright tests

View File

@ -6,7 +6,7 @@ export default defineConfig({
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: 'html',
reporter: process.env.CI ? 'github' : 'list',
/* Configure projects for major browsers */
projects: [

View File

@ -0,0 +1,25 @@
import { Center, Group, Tooltip } from '@mantine/core';
import { IconServer } from '@tabler/icons-react';
import { ColorToggle } from '../items/ColorToggle';
import { LanguageToggle } from '../items/LanguageToggle';
export function AuthFormOptions({
hostname,
toggleHostEdit
}: {
hostname: string;
toggleHostEdit: () => void;
}) {
return (
<Center mx={'md'}>
<Group>
<ColorToggle />
<LanguageToggle />
<Tooltip label={hostname}>
<IconServer onClick={toggleHostEdit} />
</Tooltip>
</Group>
</Center>
);
}

View File

@ -16,19 +16,8 @@ import { IconCheck } from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';
import { doClassicLogin, doSimpleLogin } from '../../functions/auth';
import { EditButton } from '../items/EditButton';
export function AuthenticationForm({
hostname,
editing,
setEditing,
selectElement
}: {
hostname: string;
editing: boolean;
setEditing: (value?: React.SetStateAction<boolean> | undefined) => void;
selectElement: JSX.Element;
}) {
export function AuthenticationForm() {
const classicForm = useForm({
initialValues: { username: '', password: '' }
});
@ -82,10 +71,7 @@ export function AuthenticationForm({
return (
<Paper radius="md" p="xl" withBorder>
<Text size="lg" weight={500}>
<Group>
{!editing ? hostname : selectElement}
<EditButton setEditing={setEditing} editing={editing} />
</Group>
<Trans>Welcome, log in below</Trans>
</Text>
<form onSubmit={classicForm.onSubmit(() => {})}>
{classicLoginMode ? (

View File

@ -0,0 +1,77 @@
import { Trans } from '@lingui/macro';
import { Divider, Group, Select, Text, Title } from '@mantine/core';
import { useToggle } from '@mantine/hooks';
import { IconCheck } from '@tabler/icons-react';
import { useLocalState } from '../../states/LocalState';
import { HostList } from '../../states/states';
import { EditButton } from '../items/EditButton';
import { HostOptionsForm } from './HostOptionsForm';
export function InstanceOptions({
hostKey,
ChangeHost,
setHostEdit
}: {
hostKey: string;
ChangeHost: (newHost: string) => void;
setHostEdit: () => void;
}) {
const [HostListEdit, setHostListEdit] = useToggle([false, true] as const);
const [setHost, setHostList, hostList] = useLocalState((state) => [
state.setHost,
state.setHostList,
state.hostList
]);
const hostListData = Object.keys(hostList).map((key) => ({
value: key,
label: hostList[key].name
}));
function SaveOptions(newHostList: HostList): void {
setHostList(newHostList);
if (newHostList[hostKey] === undefined) {
setHost('', '');
}
setHostListEdit();
}
return (
<>
<Title order={3}>
<Trans>Select destination instance</Trans>
</Title>
<Group>
<Group>
<Select
value={hostKey}
onChange={ChangeHost}
data={hostListData}
disabled={HostListEdit}
/>
<EditButton
setEditing={setHostListEdit}
editing={HostListEdit}
disabled={HostListEdit}
/>
</Group>
<EditButton
setEditing={setHostEdit}
editing={true}
disabled={HostListEdit}
saveIcon={<IconCheck />}
/>
</Group>
{HostListEdit && (
<>
<Divider my={'sm'} />
<Text>
<Trans>Edit possible host options</Trans>
</Text>
<HostOptionsForm data={hostList} saveOptions={SaveOptions} />
</>
)}
</>
);
}

View File

@ -10,10 +10,6 @@ export function ColorToggle() {
onClick={() => toggleColorScheme()}
size="lg"
sx={(theme) => ({
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.dark[6]
: theme.colors.gray[0],
color:
theme.colorScheme === 'dark'
? theme.colors.yellow[4]

View File

@ -4,15 +4,18 @@ import { IconDeviceFloppy, IconEdit } from '@tabler/icons-react';
export function EditButton({
setEditing,
editing,
disabled
disabled,
saveIcon
}: {
setEditing: (value?: React.SetStateAction<boolean> | undefined) => void;
editing: boolean;
disabled?: boolean;
saveIcon?: JSX.Element;
}) {
saveIcon = saveIcon || <IconDeviceFloppy />;
return (
<ActionIcon onClick={() => setEditing()} disabled={disabled}>
{editing ? <IconDeviceFloppy /> : <IconEdit />}
{editing ? saveIcon : <IconEdit />}
</ActionIcon>
);
}

View File

@ -0,0 +1,26 @@
import { Select } from '@mantine/core';
import { useEffect, useState } from 'react';
import { Locales, languages } from '../../contexts/LanguageContext';
import { useLocalState } from '../../states/LocalState';
export function LanguageSelect() {
const [value, setValue] = useState<string | null>(null);
const [locale, setLanguage] = useLocalState((state) => [
state.language,
state.setLanguage
]);
// change global language on change
useEffect(() => {
if (value === null) return;
setLanguage(value as Locales);
}, [value]);
// set language on component load
useEffect(() => {
setValue(locale);
}, [locale]);
return <Select w={80} data={languages} value={value} onChange={setValue} />;
}

View File

@ -0,0 +1,29 @@
import { ActionIcon, Group } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconLanguage } from '@tabler/icons-react';
import { LanguageSelect } from './LanguageSelect';
export function LanguageToggle() {
const [open, toggle] = useDisclosure();
return (
<Group
position="center"
style={{
border: open === true ? `1px dashed ` : ``,
margin: open === true ? 2 : 12,
padding: open === true ? 8 : 0
}}
>
<ActionIcon onClick={() => toggle.toggle()} size="lg">
<IconLanguage />
</ActionIcon>
{open && (
<Group>
<LanguageSelect />
</Group>
)}
</Group>
);
}

View File

@ -4,7 +4,6 @@ import { useNavigate, useParams } from 'react-router-dom';
import { navTabs as mainNavTabs } from '../../defaults/links';
import { InvenTreeStyle } from '../../globalStyle';
import { ColorToggle } from '../items/ColorToggle';
import { ScanButton } from '../items/ScanButton';
import { MainMenu } from './MainMenu';
import { NavHoverMenu } from './NavHoverMenu';
@ -25,7 +24,6 @@ export function Header() {
</Group>
<Group>
<ScanButton />
<ColorToggle />
<MainMenu />
</Group>
</Group>

View File

@ -3,35 +3,20 @@ import { Group, Menu, Skeleton, Text, UnstyledButton } from '@mantine/core';
import {
IconChevronDown,
IconHeart,
IconLanguage,
IconLogout,
IconSettings,
IconUserCircle
} from '@tabler/icons-react';
import { Link } from 'react-router-dom';
import { languages } from '../../contexts/LanguageContext';
import { doClassicLogout } from '../../functions/auth';
import { InvenTreeStyle } from '../../globalStyle';
import { useApiState } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState';
import { PlaceholderPill } from '../items/Placeholder';
export function MainMenu() {
const { classes, theme } = InvenTreeStyle();
const [username] = useApiState((state) => [state.user?.name]);
const [locale] = useLocalState((state) => [state.language]);
// Language
function switchLanguage() {
useLocalState.setState({
language: languages[(languages.indexOf(locale) + 1) % languages.length]
});
}
function enablePsuedo() {
useLocalState.setState({ language: 'pseudo-LOCALE' });
}
return (
<Menu width={260} position="bottom-end">
<Menu.Target>
@ -53,26 +38,15 @@ export function MainMenu() {
<Trans>Notifications</Trans>
<PlaceholderPill />
</Menu.Item>
<Menu.Item
icon={<IconUserCircle />}
component={Link}
to="/profile/user"
>
<Trans>Profile</Trans>
<Menu.Item icon={<IconUserCircle />}>
<Trans>Profile</Trans> <PlaceholderPill />
</Menu.Item>
<Menu.Label>
<Trans>Settings</Trans>
</Menu.Label>
<Menu.Item icon={<IconLanguage />} onClick={switchLanguage}>
<Trans>Current language {locale}</Trans>
</Menu.Item>
<Menu.Item icon={<IconLanguage />} onClick={enablePsuedo}>
<Trans>Switch to pseudo language</Trans>
</Menu.Item>
<Menu.Item icon={<IconSettings />}>
<Menu.Item icon={<IconSettings />} component={Link} to="/profile/user">
<Trans>Account settings</Trans>
<PlaceholderPill />
</Menu.Item>
<Menu.Item
icon={<IconLogout />}

View File

@ -1,14 +1,13 @@
import { Trans, t } from '@lingui/macro';
import { Center, Container, Group, Select, Stack, Text } from '@mantine/core';
import { t } from '@lingui/macro';
import { Center, Container } from '@mantine/core';
import { useToggle } from '@mantine/hooks';
import { useEffect } from 'react';
import { AuthFormOptions } from '../../components/forms/AuthFormOptions';
import { AuthenticationForm } from '../../components/forms/AuthenticationForm';
import { HostOptionsForm } from '../../components/forms/HostOptionsForm';
import { EditButton } from '../../components/items/EditButton';
import { InstanceOptions } from '../../components/forms/InstanceOptions';
import { defaultHostKey } from '../../defaults/defaultHostList';
import { useLocalState } from '../../states/LocalState';
import { HostList } from '../../states/states';
export default function Login() {
const [hostKey, setHost, hostList] = useLocalState((state) => [
@ -19,24 +18,12 @@ export default function Login() {
const hostname =
hostList[hostKey] === undefined ? t`No selection` : hostList[hostKey].name;
const [hostEdit, setHostEdit] = useToggle([false, true] as const);
const hostListData = Object.keys(hostList).map((key) => ({
value: key,
label: hostList[key].name
}));
const [HostListEdit, setHostListEdit] = useToggle([false, true] as const);
// Data manipulation functions
function ChangeHost(newHost: string): void {
setHost(hostList[newHost].host, newHost);
setHostEdit(false);
}
function SaveOptions(newHostList: HostList): void {
useLocalState.setState({ hostList: newHostList });
if (newHostList[hostKey] === undefined) {
setHost('', '');
}
setHostListEdit();
}
// Set default host to localhost if no host is selected
useEffect(() => {
if (hostKey === '') {
@ -44,87 +31,23 @@ export default function Login() {
}
}, []);
// Main rendering block
return (
<Center mih="100vh">
<Container w="md" miw={400}>
<Stack>
<EditHostList
hostList={hostList}
SaveOptions={SaveOptions}
HostListEdit={HostListEdit}
{hostEdit ? (
<InstanceOptions
hostKey={hostKey}
ChangeHost={ChangeHost}
setHostEdit={setHostEdit}
/>
{!HostListEdit && (
<AuthenticationForm
hostname={hostname}
editing={hostEdit}
setEditing={setHostEdit}
selectElement={
<SelectHost
hostKey={hostKey}
ChangeHost={ChangeHost}
hostListData={hostListData}
HostListEdit={HostListEdit}
hostEdit={hostEdit}
setHostListEdit={setHostListEdit}
/>
}
/>
)}
</Stack>
) : (
<>
<AuthenticationForm />
<AuthFormOptions hostname={hostname} toggleHostEdit={setHostEdit} />
</>
)}
</Container>
</Center>
);
}
const SelectHost = ({
hostKey,
ChangeHost,
hostListData,
HostListEdit,
hostEdit,
setHostListEdit
}: {
hostKey: string;
ChangeHost: (newHost: string) => void;
hostListData: any;
HostListEdit: boolean;
hostEdit: boolean;
setHostListEdit: (value?: React.SetStateAction<boolean> | undefined) => void;
}) => {
if (!hostEdit) return <></>;
return (
<Group>
<Select
value={hostKey}
onChange={ChangeHost}
data={hostListData}
disabled={HostListEdit}
/>
<EditButton
setEditing={setHostListEdit}
editing={HostListEdit}
disabled={HostListEdit}
/>
</Group>
);
};
const EditHostList = ({
hostList,
SaveOptions,
HostListEdit
}: {
hostList: HostList;
SaveOptions: (newHostList: HostList) => void;
HostListEdit: boolean;
}) => {
if (!HostListEdit) return null;
return (
<>
<Text>
<Trans>Edit host options</Trans>
</Text>
<HostOptionsForm data={hostList} saveOptions={SaveOptions} />
</>
);
};

View File

@ -16,7 +16,10 @@ 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 { useLocalState } from '../../../states/LocalState';
import { UserTheme } from './UserTheme';
export function UserPanel() {
@ -48,7 +51,7 @@ export function UserPanel() {
<UserTheme height={SECONDARY_COL_HEIGHT} />
</Grid.Col>
<Grid.Col span={6}>
<Skeleton height={SECONDARY_COL_HEIGHT} />
<DisplaySettings height={SECONDARY_COL_HEIGHT} />
</Grid.Col>
<Grid.Col span={6}>
<Skeleton height={SECONDARY_COL_HEIGHT} />
@ -122,3 +125,34 @@ export function UserInfo({ data }: { data: any }) {
</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 />
<Button onClick={enablePseudoLang} variant="light">
<Trans>Use pseudo language</Trans>
</Button>
</Stack>
</Group>
</Container>
);
}

View File

@ -13,7 +13,9 @@ interface LocalStateProps {
setHost: (newHost: string, newHostKey: string) => void;
hostKey: string;
hostList: HostList;
setHostList: (newHostList: HostList) => void;
language: Locales;
setLanguage: (newLanguage: Locales) => void;
// theme
primaryColor: string;
whiteColor: string;
@ -33,7 +35,9 @@ export const useLocalState = create<LocalStateProps>()(
set({ host: newHost, hostKey: newHostKey }),
hostKey: '',
hostList: {},
setHostList: (newHostList) => set({ hostList: newHostList }),
language: 'en',
setLanguage: (newLanguage) => set({ language: newLanguage }),
//theme
primaryColor: 'indigo',
whiteColor: '#fff',

View File

@ -129,7 +129,7 @@ def node_available(versions: bool = False, bypass_yarn: bool = False):
print('Node is available but yarn is not. Install yarn if you wish to build the frontend.')
# Return the result
return ret((not yarn_passes or not node_version), node_version, yarn_version)
return ret(yarn_passes and node_version, node_version, yarn_version)
def check_file_existance(filename: str, overwrite: bool = False):