mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 23:45:48 +01:00
🔀 Merge branch 'dev' into tests/add-tests
This commit is contained in:
@@ -57,7 +57,12 @@ export default function ConfigChanger() {
|
||||
size="lg"
|
||||
radius="md"
|
||||
>
|
||||
<Notification loading title={t('configSelect.loadingNew')} radius="md" disallowClose>
|
||||
<Notification
|
||||
loading
|
||||
title={t('configSelect.loadingNew')}
|
||||
radius="md"
|
||||
withCloseButton={false}
|
||||
>
|
||||
{t('configSelect.pleaseWait')}
|
||||
</Notification>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {
|
||||
Accordion,
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Badge,
|
||||
Button,
|
||||
createStyles,
|
||||
Divider,
|
||||
Grid,
|
||||
Group,
|
||||
HoverCard,
|
||||
Kbd,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconAnchor,
|
||||
@@ -35,6 +36,8 @@ import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
|
||||
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
||||
import { useColorTheme } from '../../../../tools/color';
|
||||
import Tip from '../../../layout/Tip';
|
||||
import { usePrimaryGradient } from '../../../layout/useGradient';
|
||||
import Credits from '../../../Settings/Common/Credits';
|
||||
|
||||
@@ -50,6 +53,23 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
||||
const informations = useInformationTableItems(newVersionAvailable);
|
||||
const { t } = useTranslation(['common', 'layout/modals/about']);
|
||||
|
||||
const keybinds = [
|
||||
{ key: 'Mod + J', shortcut: 'Toggle light/dark mode' },
|
||||
{ key: 'Mod + K', shortcut: 'Focus on search bar' },
|
||||
{ key: 'Mod + B', shortcut: 'Open docker widget' },
|
||||
{ key: 'Mod + E', shortcut: 'Toggle Edit mode' },
|
||||
];
|
||||
const rows = keybinds.map((element) => (
|
||||
<tr key={element.key}>
|
||||
<td>
|
||||
<Kbd>{element.key}</Kbd>
|
||||
</td>
|
||||
<td>
|
||||
<Text>{element.shortcut}</Text>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={() => closeModal()}
|
||||
@@ -76,7 +96,7 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
||||
<Trans i18nKey="layout/modals/about:description" />
|
||||
</Text>
|
||||
|
||||
<Table mb="lg" striped highlightOnHover withBorder>
|
||||
<Table mb="lg" highlightOnHover withBorder>
|
||||
<tbody>
|
||||
{informations.map((item, index) => (
|
||||
<tr key={index}>
|
||||
@@ -100,8 +120,26 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<Accordion mb={5} variant="contained" radius="md">
|
||||
<Accordion.Item value="keybinds">
|
||||
<Accordion.Control icon={<IconKey size={20} />}>
|
||||
{t('layout/modals/about:keybinds')}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Table mb={5}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('layout/modals/about:key')}</th>
|
||||
<th>{t('layout/modals/about:action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</Table>
|
||||
<Tip>{t('layout/modals/about:tip')}</Tip>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
|
||||
<Divider variant="dashed" mb="md" />
|
||||
<Title order={6} mb="xs" align="center">
|
||||
{t('layout/modals/about:contact')}
|
||||
</Title>
|
||||
@@ -161,9 +199,9 @@ interface ExtendedInitOptions extends InitOptions {
|
||||
}
|
||||
|
||||
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
||||
const colorGradiant = usePrimaryGradient();
|
||||
const { attributes } = usePackageAttributesStore();
|
||||
const { editModeEnabled } = useEditModeInformationStore();
|
||||
const { primaryColor } = useColorTheme();
|
||||
|
||||
const { configVersion } = useConfigContext();
|
||||
const { configs } = useConfigStore();
|
||||
@@ -177,15 +215,19 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconKey size={20} />,
|
||||
label: 'experimental_disableEditMode',
|
||||
content: (
|
||||
<Stack>
|
||||
<Tooltip
|
||||
color="red"
|
||||
withinPortal
|
||||
width={300}
|
||||
multiline
|
||||
withArrow
|
||||
label="This is an experimental feature, where the edit mode is disabled entirely - no config
|
||||
modifications are possbile anymore. All update requests for the config will be dropped
|
||||
on the API. This will be removed in future versions, as Homarr will receive a proper
|
||||
authentication system, which will make this obsolete."
|
||||
>
|
||||
<Badge color="red">WARNING</Badge>
|
||||
<Text color="red" size="xs">
|
||||
This is an experimental feature, where the edit mode is disabled entirely - no config
|
||||
modifications are possbile anymore. All update requests for the config will be dropped
|
||||
on the API. This will be removed in future versions, as Homarr will receive a proper
|
||||
authentication system, which will make this obsolete.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -201,7 +243,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconLanguage size={20} />,
|
||||
label: 'i18n',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{usedI18nNamespaces.length}
|
||||
</Badge>
|
||||
),
|
||||
@@ -210,7 +252,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconVocabulary size={20} />,
|
||||
label: 'locales',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{initOptions.locales.length}
|
||||
</Badge>
|
||||
),
|
||||
@@ -223,7 +265,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconSchema size={20} />,
|
||||
label: 'configurationSchemaVersion',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{configVersion}
|
||||
</Badge>
|
||||
),
|
||||
@@ -232,7 +274,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconFile size={20} />,
|
||||
label: 'configurationsCount',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{configs.length}
|
||||
</Badge>
|
||||
),
|
||||
@@ -242,7 +284,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
label: 'version',
|
||||
content: (
|
||||
<Group position="right">
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{attributes.packageVersion ?? 'Unknown'}
|
||||
</Badge>
|
||||
{newVersionAvailable && (
|
||||
@@ -282,7 +324,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
||||
icon: <IconAnchor size={20} />,
|
||||
label: 'nodeEnvironment',
|
||||
content: (
|
||||
<Badge variant="gradient" gradient={colorGradiant}>
|
||||
<Badge variant="light" color={primaryColor}>
|
||||
{attributes.environment}
|
||||
</Badge>
|
||||
),
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Grid,
|
||||
Group,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
Text,
|
||||
@@ -40,7 +39,7 @@ export const GenericSecretInput = ({
|
||||
|
||||
const Icon = setIcon;
|
||||
|
||||
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(false);
|
||||
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(!secretIsPresent);
|
||||
const { t } = useTranslation(['layout/modals/add-app', 'common']);
|
||||
|
||||
return (
|
||||
@@ -51,26 +50,26 @@ export const GenericSecretInput = ({
|
||||
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light" size="lg">
|
||||
<Icon size={18} />
|
||||
</ThemeIcon>
|
||||
<Stack spacing={0}>
|
||||
<Flex justify="start" align="start" direction="column">
|
||||
<Group spacing="xs">
|
||||
<Title className={classes.subtitle} order={6}>
|
||||
{t(label)}
|
||||
</Title>
|
||||
|
||||
<Group spacing="xs">
|
||||
{secretIsPresent ? (
|
||||
<Badge className={classes.textTransformUnset} color="green" variant="dot">
|
||||
{t('integration.type.defined')}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className={classes.textTransformUnset} color="red" variant="dot">
|
||||
{t('integration.type.undefined')}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge
|
||||
className={classes.textTransformUnset}
|
||||
color={secretIsPresent ? 'green' : 'red'}
|
||||
variant="dot"
|
||||
>
|
||||
{secretIsPresent
|
||||
? t('integration.type.defined')
|
||||
: t('integration.type.undefined')}
|
||||
</Badge>
|
||||
{type === 'private' ? (
|
||||
<Tooltip
|
||||
label={t('integration.type.explanationPrivate')}
|
||||
width={200}
|
||||
width={400}
|
||||
multiline
|
||||
withinPortal
|
||||
withArrow
|
||||
@@ -82,7 +81,7 @@ export const GenericSecretInput = ({
|
||||
) : (
|
||||
<Tooltip
|
||||
label={t('integration.type.explanationPublic')}
|
||||
width={200}
|
||||
width={400}
|
||||
multiline
|
||||
withinPortal
|
||||
withArrow
|
||||
@@ -94,29 +93,20 @@ export const GenericSecretInput = ({
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
<Text size="xs" color="dimmed">
|
||||
<Text size="xs" color="dimmed" w={400}>
|
||||
{type === 'private'
|
||||
? 'Private: Once saved, you cannot read out this value again'
|
||||
: 'Public: Can be read out repeatedly'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
<Grid.Col xs={12} md={6}>
|
||||
<Flex gap={10} justify="end" align="end">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisplayUpdateField(false);
|
||||
onClickUpdateButton(undefined);
|
||||
}}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
px="xl"
|
||||
>
|
||||
{t('integration.secrets.clear')}
|
||||
</Button>
|
||||
{displayUpdateField === true ? (
|
||||
<PasswordInput
|
||||
required
|
||||
defaultValue={value}
|
||||
placeholder="new secret"
|
||||
styles={{ root: { width: 200 } }}
|
||||
{...props}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const GenericAvailableElementType = ({
|
||||
: image;
|
||||
|
||||
return (
|
||||
<Grid.Col span={3}>
|
||||
<Grid.Col span="auto">
|
||||
<Card style={{ height: '100%' }}>
|
||||
<Stack justify="space-between" style={{ height: '100%' }}>
|
||||
<Stack spacing="xs">
|
||||
|
||||
@@ -19,7 +19,7 @@ export const AvailableStaticTypes = ({ onClickBack }: AvailableStaticTypesProps)
|
||||
they don't integrate with any apps and their content never changes.
|
||||
</Text>
|
||||
|
||||
<Grid>
|
||||
<Grid grow>
|
||||
<GenericAvailableElementType
|
||||
name="Static Text"
|
||||
description="Display a fixed string on your dashboard"
|
||||
|
||||
@@ -93,12 +93,10 @@ export const AppTile = ({ className, app }: AppTileProps) => {
|
||||
|
||||
const useStyles = createStyles((theme, _params, getRef) => ({
|
||||
image: {
|
||||
ref: getRef('image'),
|
||||
maxHeight: '90%',
|
||||
maxWidth: '90%',
|
||||
},
|
||||
appName: {
|
||||
ref: getRef('appName'),
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
button: {
|
||||
|
||||
@@ -18,12 +18,11 @@ interface GridstackStoreType {
|
||||
|
||||
export const useNamedWrapperColumnCount = (): 'small' | 'medium' | 'large' | null => {
|
||||
const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth);
|
||||
const { sm, xl } = useMantineTheme().breakpoints;
|
||||
if (!mainAreaWidth) return null;
|
||||
|
||||
if (mainAreaWidth >= xl) return 'large';
|
||||
if (mainAreaWidth >= 1400) return 'large';
|
||||
|
||||
if (mainAreaWidth >= sm) return 'medium';
|
||||
if (mainAreaWidth >= 800) return 'medium';
|
||||
|
||||
return 'small';
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function CommonSettings() {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
|
||||
<ScrollArea style={{ height: height - 100 }} scrollbarSize={5}>
|
||||
<Stack>
|
||||
<SearchEngineSelector searchEngine={config.settings.common.searchEngine} />
|
||||
<Space />
|
||||
|
||||
@@ -60,6 +60,7 @@ export const SearchEngineSelector = ({ searchEngine }: Props) => {
|
||||
<Space mb="md" />
|
||||
<TextInput
|
||||
label={t('customEngine.label')}
|
||||
name={t('configurationName')}
|
||||
description={t('tips.placeholderTip')}
|
||||
placeholder={t('customEngine.placeholder')}
|
||||
value={searchUrl}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function CustomizationSettings() {
|
||||
const { t } = useTranslation('settings/customization/general');
|
||||
|
||||
return (
|
||||
<ScrollArea style={{ height: height - 100 }} offsetScrollbars>
|
||||
<ScrollArea style={{ height: height - 100 }} scrollbarSize={5}>
|
||||
<Stack mt="xs" mb="md" spacing="xs">
|
||||
<Text color="dimmed">{t('text')}</Text>
|
||||
<CustomizationSettingsAccordeon />
|
||||
|
||||
@@ -41,7 +41,7 @@ export function SettingsDrawer({
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
size="xl"
|
||||
size="lg"
|
||||
padding="lg"
|
||||
position="right"
|
||||
title={<Title order={5}>{t('title')}</Title>}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ToggleEditModeAction = () => {
|
||||
const { config } = useConfigContext();
|
||||
const { classes } = useCardStyles(true);
|
||||
|
||||
useHotkeys([['ctrl+E', toggleEditMode]]);
|
||||
useHotkeys([['mod+E', toggleEditMode]]);
|
||||
|
||||
useWindowEvent('beforeunload', (event: BeforeUnloadEvent) => {
|
||||
if (enabled) {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function Header(props: any) {
|
||||
>
|
||||
<Search />
|
||||
{!editModeEnabled && <ToggleEditModeAction />}
|
||||
<DockerMenuButton />
|
||||
{!editModeEnabled && <DockerMenuButton />}
|
||||
<Indicator
|
||||
size={15}
|
||||
color="blue"
|
||||
|
||||
@@ -181,7 +181,6 @@ export function Search() {
|
||||
shadow="md"
|
||||
radius="md"
|
||||
zIndex={100}
|
||||
transition="pop-top-right"
|
||||
>
|
||||
<Popover.Target>
|
||||
<Autocomplete
|
||||
@@ -297,7 +296,7 @@ export function Search() {
|
||||
setSearchEngine(item);
|
||||
showNotification({
|
||||
radius: 'lg',
|
||||
disallowClose: true,
|
||||
withCloseButton: false,
|
||||
id: 'spotlight',
|
||||
autoClose: 1000,
|
||||
icon: <ActionIcon size="sm">{item.icon}</ActionIcon>,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AboutModal } from '../../Dashboard/Modals/AboutModal/AboutModal';
|
||||
import { SettingsDrawer } from '../../Settings/SettingsDrawer';
|
||||
import { useCardStyles } from '../useCardStyles';
|
||||
import { ColorSchemeSwitch } from './SettingsMenu/ColorSchemeSwitch';
|
||||
import { EditModeToggle } from './SettingsMenu/EditModeToggle';
|
||||
|
||||
export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
|
||||
const [drawerOpened, drawer] = useDisclosure(false);
|
||||
@@ -25,6 +26,7 @@ export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: str
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<ColorSchemeSwitch />
|
||||
<EditModeToggle />
|
||||
<Menu.Divider />
|
||||
{!editModeEnabled && (
|
||||
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>
|
||||
|
||||
78
src/components/layout/header/SettingsMenu/EditModeToggle.tsx
Normal file
78
src/components/layout/header/SettingsMenu/EditModeToggle.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Button, Code, Menu, PasswordInput, Stack, Text } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { openModal } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconEdit, IconEditOff } from '@tabler/icons';
|
||||
import axios from 'axios';
|
||||
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
|
||||
|
||||
function ModalContent() {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
triedPassword: '',
|
||||
},
|
||||
});
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
axios
|
||||
.post('/api/configs/tryPassword', { tried: values.triedPassword, type: 'edit' })
|
||||
.then((res) => {
|
||||
showNotification({
|
||||
title: 'Success',
|
||||
message: 'Successfully toggled edit mode, reloading the page...',
|
||||
color: 'green',
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
})
|
||||
.catch((_) => {
|
||||
showNotification({
|
||||
title: 'Error',
|
||||
message: 'Failed to toggle edit mode, please try again.',
|
||||
color: 'red',
|
||||
});
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<Text size="sm">
|
||||
In order to toggle edit mode, you need to enter the password you entered in the
|
||||
environment variable named <Code>EDIT_MODE_PASSWORD</Code> . If it is not set, you are not
|
||||
able to toggle edit mode on and off.
|
||||
</Text>
|
||||
<PasswordInput
|
||||
id="triedPassword"
|
||||
label="Edit password"
|
||||
autoFocus
|
||||
required
|
||||
{...form.getInputProps('triedPassword')}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function EditModeToggle() {
|
||||
const { editModeEnabled } = useEditModeInformationStore();
|
||||
const Icon = editModeEnabled ? IconEdit : IconEditOff;
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
closeMenuOnClick={false}
|
||||
icon={<Icon strokeWidth={1.2} size={18} />}
|
||||
onClick={() =>
|
||||
openModal({
|
||||
title: 'Toggle edit mode',
|
||||
centered: true,
|
||||
size: 'lg',
|
||||
children: <ModalContent />,
|
||||
})
|
||||
}
|
||||
>
|
||||
{editModeEnabled ? 'Enable edit mode' : 'Disable edit mode'}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user