mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
Update default config
This commit is contained in:
@@ -1,23 +1,30 @@
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"schemaVersion": 1,
|
||||
"configProperties": {
|
||||
"name": "default"
|
||||
},
|
||||
"categories": [],
|
||||
"categories": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f",
|
||||
"position": 0,
|
||||
"name": "Example Category"
|
||||
}
|
||||
],
|
||||
"wrappers": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e",
|
||||
"id": "default",
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"apps": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33a",
|
||||
"name": "Documentation",
|
||||
"url": "https://homarr.dev",
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
|
||||
"name": "Community",
|
||||
"url": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://homarr.dev",
|
||||
"isOpeningInNewTab": true
|
||||
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"isOpeningNewTab": true,
|
||||
"externalUrl": "https://discord.com/invite/aCsmEV5RgA"
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
@@ -26,7 +33,7 @@
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "/imgs/logo/logo.png"
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/discord.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
@@ -35,7 +42,46 @@
|
||||
"area": {
|
||||
"type": "wrapper",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
|
||||
"id": "default"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
|
||||
"name": "Donate",
|
||||
"url": "https://ko-fi.com/ajnart",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://ko-fi.com/ajnart",
|
||||
"externalUrl": "https://ko-fi.com/ajnart",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "wrapper",
|
||||
"properties": {
|
||||
"id": "default"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
@@ -55,7 +101,8 @@
|
||||
"url": "https://github.com/ajnart/homarr",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://github.com/ajnart/homarr",
|
||||
"isOpeningInNewTab": true
|
||||
"externalUrl": "https://github.com/ajnart/homarr",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
@@ -73,107 +120,7 @@
|
||||
"area": {
|
||||
"type": "wrapper",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
|
||||
"name": "Community",
|
||||
"url": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"isOpeningInNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/discord.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "wrapper",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
|
||||
"name": "Donate",
|
||||
"url": "https://ko-fi.com/ajnart",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://ko-fi.com/ajnart",
|
||||
"isOpeningInNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "wrapper",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 9,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [
|
||||
{
|
||||
"id": "date",
|
||||
"properties": {
|
||||
"display24HourFormat": true
|
||||
},
|
||||
"area": {
|
||||
"type": "wrapper",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
|
||||
"id": "default"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
@@ -181,11 +128,227 @@
|
||||
"x": 0,
|
||||
"y": 3
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5df743d9-5cb1-457c-85d2-64ff86855652",
|
||||
"name": "Your app",
|
||||
"url": "https://homarr.dev",
|
||||
"appearance": {
|
||||
"iconUrl": "/imgs/logo/logo.png"
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": []
|
||||
},
|
||||
"behaviour": {
|
||||
"isOpeningNewTab": true,
|
||||
"externalUrl": "https://homarr.dev"
|
||||
},
|
||||
"area": {
|
||||
"type": "wrapper",
|
||||
"properties": {
|
||||
"id": "default"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 15,
|
||||
"y": 5
|
||||
},
|
||||
"size": {
|
||||
"width": 5,
|
||||
"height": 4
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33a",
|
||||
"name": "Documentation",
|
||||
"url": "https://homarr.dev",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://homarr.dev",
|
||||
"externalUrl": "https://homarr.dev",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "/imgs/logo/logo.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "wrapper",
|
||||
"properties": {
|
||||
"id": "default"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 3
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "76217a87-7151-42d0-b0cf-1b72aef63f83",
|
||||
"name": "Small app",
|
||||
"url": "https://homarr.dev",
|
||||
"appearance": {
|
||||
"iconUrl": "/imgs/logo/logo.png"
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": []
|
||||
},
|
||||
"behaviour": {
|
||||
"isOpeningNewTab": true,
|
||||
"externalUrl": "https://homarr.dev"
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 17,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 2
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e41a11f5-9c6e-41bc-ac0e-4c4c47582faa",
|
||||
"name": "Your app",
|
||||
"url": "https://homarr.dev",
|
||||
"appearance": {
|
||||
"iconUrl": "/imgs/logo/logo.png"
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"okStatus": []
|
||||
},
|
||||
"behaviour": {
|
||||
"isOpeningNewTab": true,
|
||||
"externalUrl": ""
|
||||
},
|
||||
"area": {
|
||||
"type": "wrapper",
|
||||
"properties": {
|
||||
"id": "default"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 2
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [
|
||||
{
|
||||
"id": "weather",
|
||||
"properties": {
|
||||
"displayInFahrenheit": false,
|
||||
"location": "Paris"
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 4,
|
||||
"height": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "date",
|
||||
"properties": {
|
||||
"display24HourFormat": true
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 4,
|
||||
"height": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "calendar",
|
||||
"properties": {
|
||||
"sundayStart": false
|
||||
},
|
||||
"area": {
|
||||
"type": "wrapper",
|
||||
"properties": {
|
||||
"id": "default"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"location": {
|
||||
"x": 15,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 5,
|
||||
"height": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
@@ -20,7 +20,6 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
||||
description={t('general.appname.description')}
|
||||
placeholder="My example app"
|
||||
variant="default"
|
||||
mb="md"
|
||||
withAsterisk
|
||||
required
|
||||
{...form.getInputProps('name')}
|
||||
@@ -45,7 +44,7 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
||||
description={t('general.externalAddress.description')}
|
||||
placeholder="https://homarr.mywebsite.com/"
|
||||
variant="default"
|
||||
mb="md"
|
||||
required
|
||||
{...form.getInputProps('behaviour.externalUrl')}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ContextModalProps } from '@mantine/modals';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import Widgets from '../../../../widgets';
|
||||
import type { IWidgetOptionValue } from '../../../../widgets/widgets';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { IWidget } from '../../../../widgets/widgets';
|
||||
@@ -23,6 +24,8 @@ export const WidgetsEditModal = ({
|
||||
const [moduleProperties, setModuleProperties] = useState(innerProps.options);
|
||||
const items = Object.entries(moduleProperties ?? {}) as [string, IntegrationOptionsValueType][];
|
||||
|
||||
// Find the Key in the "Widgets" Object that matches the widgetId
|
||||
const currentWidgetDefinition = Widgets[innerProps.widgetId as keyof typeof Widgets];
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
@@ -63,33 +66,38 @@ export const WidgetsEditModal = ({
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{items.map(([key, value]) => (
|
||||
<>
|
||||
{typeof value === 'boolean' ? (
|
||||
<Switch
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
checked={value}
|
||||
onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
|
||||
/>
|
||||
) : null}
|
||||
{typeof value === 'string' ? (
|
||||
<TextInput
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
value={value}
|
||||
onChange={(ev) => handleChange(key, ev.currentTarget.value)}
|
||||
/>
|
||||
) : null}
|
||||
{typeof value === 'object' && Array.isArray(value) ? (
|
||||
<MultiSelect
|
||||
data={getMutliselectData(key)}
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
value={value}
|
||||
onChange={(v) => handleChange(key, v)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
))}
|
||||
|
||||
{items.map(([key, value]) => {
|
||||
const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue;
|
||||
switch (option.type) {
|
||||
case 'switch':
|
||||
return (
|
||||
<Switch
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
checked={value as boolean}
|
||||
onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
|
||||
/>
|
||||
);
|
||||
case 'text':
|
||||
return (
|
||||
<TextInput
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
value={value as string}
|
||||
onChange={(ev) => handleChange(key, ev.currentTarget.value)}
|
||||
/>
|
||||
);
|
||||
case 'multi-select':
|
||||
return (
|
||||
<MultiSelect
|
||||
data={getMutliselectData(key)}
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
value={value as string[]}
|
||||
onChange={(v) => handleChange(key, v)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
<Group position="right">
|
||||
<Button onClick={() => context.closeModal(id)} variant="light">
|
||||
{t('common:cancel')}
|
||||
@@ -99,3 +107,41 @@ export const WidgetsEditModal = ({
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
// <Stack>
|
||||
// {items.map(([key, value]) => (
|
||||
// <>
|
||||
// {typeof value === 'boolean' ? (
|
||||
// <Switch
|
||||
// label={t(`descriptor.settings.${key}.label`)}
|
||||
// checked={value}
|
||||
// onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
|
||||
// />
|
||||
// ) : null}
|
||||
// {typeof value === 'string' ? (
|
||||
// <TextInput
|
||||
// label={t(`descriptor.settings.${key}.label`)}
|
||||
// value={value}
|
||||
// onChange={(ev) => handleChange(key, ev.currentTarget.value)}
|
||||
// />
|
||||
// ) : null}
|
||||
// {typeof value === 'object' && Array.isArray(value) ? (
|
||||
// <MultiSelect
|
||||
// data={getMutliselectData(key)}
|
||||
// label={t(`descriptor.settings.${key}.label`)}
|
||||
// value={value}
|
||||
// onChange={(v) => handleChange(key, v)}
|
||||
// />
|
||||
// ) : null}
|
||||
// </>
|
||||
// ))}
|
||||
|
||||
// <Group position="right">
|
||||
// <Button onClick={() => context.closeModal(id)} variant="light">
|
||||
// {t('common:cancel')}
|
||||
// </Button>
|
||||
// <Button onClick={handleSave}>{t('common:save')}</Button>
|
||||
// </Group>
|
||||
// </Stack>
|
||||
// );
|
||||
// };
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
IconRowInsertTop,
|
||||
IconRowInsertBottom,
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
} from '@tabler/icons';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { CategoryType } from '../../../../types/category';
|
||||
@@ -17,11 +18,11 @@ interface CategoryEditMenuProps {
|
||||
|
||||
export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
|
||||
const { name: configName } = useConfigContext();
|
||||
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit } =
|
||||
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit, remove } =
|
||||
useCategoryActions(configName, category);
|
||||
|
||||
return (
|
||||
<Menu withinPortal>
|
||||
<Menu withinPortal position="left-start" withArrow>
|
||||
<Menu.Target>
|
||||
<ActionIcon>
|
||||
<IconDots />
|
||||
@@ -31,6 +32,9 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
|
||||
<Menu.Item icon={<IconEdit size={20} />} onClick={edit}>
|
||||
Edit
|
||||
</Menu.Item>
|
||||
<Menu.Item icon={<IconTrash size={20} />} onClick={remove}>
|
||||
Remove
|
||||
</Menu.Item>
|
||||
<Menu.Label>Change positon</Menu.Label>
|
||||
<Menu.Item icon={<IconTransitionTop size={20} />} onClick={moveCategoryUp}>
|
||||
Move up
|
||||
|
||||
@@ -39,7 +39,7 @@ export const CategoryEditModal = ({
|
||||
<TextInput data-autoFocus {...form.getInputProps('name')} label="Name of category" />
|
||||
|
||||
<Group mt="md" grow>
|
||||
<Button onClick={() => context.closeModal(id)} variant="light" color="gray">
|
||||
<Button onClick={() => context.closeModal(id)} variant="filled" color="gray">
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button type="submit">{t('save')}</Button>
|
||||
|
||||
@@ -176,6 +176,37 @@ export const useCategoryActions = (configName: string | undefined, category: Cat
|
||||
);
|
||||
};
|
||||
|
||||
// Removes the current category
|
||||
const remove = () => {
|
||||
if (!configName) return;
|
||||
updateConfig(
|
||||
configName,
|
||||
(previous) => {
|
||||
const currentItem = previous.categories.find((x) => x.id === category.id);
|
||||
if (!currentItem) return previous;
|
||||
// Find the main wrapper
|
||||
const mainWrapper = previous.wrappers.find((x) => x.position === 1);
|
||||
|
||||
// Check that the app has an area.type or "category" and that the area.id is the current category
|
||||
const appsToMove = previous.apps.filter(
|
||||
(x) => x.area && x.area.type === 'category' && x.area.properties.id === currentItem.id
|
||||
);
|
||||
appsToMove.forEach((x) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
x.area = { type: 'wrapper', properties: { id: mainWrapper?.id ?? 'default' } };
|
||||
});
|
||||
|
||||
return {
|
||||
...previous,
|
||||
apps: previous.apps,
|
||||
categories: previous.categories.filter((x) => x.id !== category.id),
|
||||
wrappers: previous.wrappers.filter((x) => x.position !== currentItem.position),
|
||||
};
|
||||
},
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
const edit = async () => {
|
||||
openContextModalGeneric<CategoryEditModalInnerProps>({
|
||||
modal: 'categoryEditModal',
|
||||
@@ -201,6 +232,7 @@ export const useCategoryActions = (configName: string | undefined, category: Cat
|
||||
addCategoryBelow,
|
||||
moveCategoryUp,
|
||||
moveCategoryDown,
|
||||
remove,
|
||||
edit,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,10 +15,8 @@ export function Logo({ size = 'md', withoutText = false }: LogoProps) {
|
||||
<Group spacing={size === 'md' ? 'xs' : 4} noWrap>
|
||||
<Image
|
||||
width={size === 'md' ? 50 : 12}
|
||||
src={config?.settings.customization.logoImageUrl || '/imgs/logo/logo.png'}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
src={config?.settings.customization.logoImageUrl || '/imgs/logo/logo-color.svg'}
|
||||
alt="Homarr Logo"
|
||||
/>
|
||||
{withoutText ? null : (
|
||||
<Text
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ActionIcon, Button, Popover, Text, Tooltip } from '@mantine/core';
|
||||
import { IconEditCircle, IconEditCircleOff, IconX } from '@tabler/icons';
|
||||
import axios from 'axios';
|
||||
import Consola from 'consola';
|
||||
import { getCookie } from 'cookies-next';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useConfigContext } from '../../../../../config/provider';
|
||||
import { useScreenSmallerThan } from '../../../../../hooks/useScreenSmallerThan';
|
||||
|
||||
import { useEditModeStore } from '../../../../Dashboard/Views/useEditModeStore';
|
||||
@@ -13,9 +17,18 @@ export const ToggleEditModeAction = () => {
|
||||
const { t } = useTranslation('layout/header/actions/toggle-edit-mode');
|
||||
|
||||
const smallerThanSm = useScreenSmallerThan('sm');
|
||||
const { config } = useConfigContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled || config === undefined || config?.schemaVersion === undefined) return;
|
||||
const configName = getCookie('config-name')?.toString() ?? 'default';
|
||||
axios.put(`/api/configs/${configName}`, { ...config });
|
||||
Consola.log('Saved config to server', configName);
|
||||
}, [enabled]);
|
||||
|
||||
const toggleButtonClicked = () => {
|
||||
toggleEditMode();
|
||||
|
||||
setPopoverManuallyHidden(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { Results } from 'sabnzbd-api';
|
||||
import {
|
||||
UsenetQueueRequestParams,
|
||||
UsenetQueueResponse,
|
||||
} from '../pages/api/modules/usenet/queue';
|
||||
import { UsenetQueueRequestParams, UsenetQueueResponse } from '../pages/api/modules/usenet/queue';
|
||||
import {
|
||||
UsenetHistoryRequestParams,
|
||||
UsenetHistoryResponse,
|
||||
|
||||
@@ -11,6 +11,7 @@ export function middleware(req: NextRequest, ev: NextFetchEvent) {
|
||||
(url.pathname.includes('/_next/') && !url.pathname.includes('/pages/')) ||
|
||||
url.pathname === '/favicon.ico' ||
|
||||
url.pathname === '/404' ||
|
||||
url.pathname === '/migrate' ||
|
||||
url.pathname.includes('pages/_app'));
|
||||
if (!skipURL && !isCorrectPassword && process.env.PASSWORD) {
|
||||
url.pathname = '/login';
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function getServerSideProps({
|
||||
return {
|
||||
props: {
|
||||
config: {
|
||||
schemaVersion: '1.0',
|
||||
schemaVersion: 1,
|
||||
configProperties: {
|
||||
name: 'Default Configuration',
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import { GetServerSidePropsContext } from 'next';
|
||||
import { SSRConfig } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
import fs from 'fs';
|
||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||
import { Dashboard } from '../components/Dashboard/Dashboard';
|
||||
import Layout from '../components/layout/Layout';
|
||||
@@ -24,6 +25,24 @@ export async function getServerSideProps({
|
||||
res,
|
||||
locale,
|
||||
}: GetServerSidePropsContext): Promise<{ props: ServerSideProps }> {
|
||||
// Check that all the json files in the /data/configs folder are migrated
|
||||
// If not, redirect to the migrate page
|
||||
const configs = await fs.readdirSync('./data/configs');
|
||||
if (
|
||||
!configs.every(
|
||||
(config) => JSON.parse(fs.readFileSync(`./data/configs/${config}`, 'utf8')).schemaVersion
|
||||
)
|
||||
) {
|
||||
// Replace the current page with the migrate page but don't redirect
|
||||
// This is to prevent the user from seeing the redirect
|
||||
res.writeHead(302, {
|
||||
Location: '/migrate',
|
||||
});
|
||||
res.end();
|
||||
|
||||
return { props: {} as ServerSideProps };
|
||||
}
|
||||
|
||||
let configName = getCookie('config-name', { req, res });
|
||||
const configLocale = getCookie('config-locale', { req, res });
|
||||
if (!configName) {
|
||||
|
||||
366
src/pages/migrate.tsx
Normal file
366
src/pages/migrate.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import fs from 'fs';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
|
||||
import {
|
||||
createStyles,
|
||||
Title,
|
||||
Text,
|
||||
Container,
|
||||
Group,
|
||||
Stepper,
|
||||
useMantineTheme,
|
||||
Header,
|
||||
AppShell,
|
||||
useMantineColorScheme,
|
||||
Switch,
|
||||
Box,
|
||||
Button,
|
||||
Alert,
|
||||
Badge,
|
||||
List,
|
||||
Loader,
|
||||
Paper,
|
||||
Progress,
|
||||
Space,
|
||||
Stack,
|
||||
ThemeIcon,
|
||||
Anchor,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconSun,
|
||||
IconMoonStars,
|
||||
IconCheck,
|
||||
IconAlertCircle,
|
||||
IconCircleCheck,
|
||||
IconBrandDiscord,
|
||||
} from '@tabler/icons';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Logo } from '../components/layout/Logo';
|
||||
import { usePrimaryGradient } from '../components/layout/useGradient';
|
||||
import { migrateConfig } from '../tools/config/migrateConfig';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
backgroundColor: theme.fn.variant({ variant: 'light', color: theme.primaryColor }).background,
|
||||
},
|
||||
|
||||
label: {
|
||||
textAlign: 'center',
|
||||
color: theme.colors[theme.primaryColor][8],
|
||||
fontWeight: 900,
|
||||
fontSize: 110,
|
||||
lineHeight: 1,
|
||||
marginBottom: theme.spacing.xl * 1.5,
|
||||
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
fontSize: 60,
|
||||
},
|
||||
},
|
||||
|
||||
title: {
|
||||
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
|
||||
textAlign: 'center',
|
||||
fontWeight: 900,
|
||||
fontSize: 38,
|
||||
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
fontSize: 32,
|
||||
},
|
||||
},
|
||||
|
||||
card: {
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
padding: theme.spacing.xl,
|
||||
},
|
||||
|
||||
icon: {
|
||||
position: 'absolute',
|
||||
top: -ICON_SIZE / 3,
|
||||
left: `calc(50% - ${ICON_SIZE / 2}px)`,
|
||||
},
|
||||
|
||||
description: {
|
||||
maxWidth: 700,
|
||||
margin: 'auto',
|
||||
marginTop: theme.spacing.xl,
|
||||
marginBottom: theme.spacing.xl * 1.5,
|
||||
},
|
||||
}));
|
||||
|
||||
export default function ServerError({ configs }: { configs: any }) {
|
||||
const { classes } = useStyles();
|
||||
const [active, setActive] = React.useState(0);
|
||||
const gradient = usePrimaryGradient();
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
const [isUpgrading, setIsUpgrading] = React.useState(false);
|
||||
const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current));
|
||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
padding={0}
|
||||
header={
|
||||
<Header
|
||||
height={60}
|
||||
px="xs"
|
||||
styles={(theme) => ({
|
||||
main: {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0],
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Group position="apart" align="start">
|
||||
<Box mt="xs">
|
||||
<Logo />
|
||||
</Box>
|
||||
<SwitchToggle />
|
||||
</Group>
|
||||
{/* Header content */}
|
||||
</Header>
|
||||
}
|
||||
styles={(theme) => ({
|
||||
main: {
|
||||
backgroundColor: theme.fn.variant({ variant: 'light', color: theme.primaryColor })
|
||||
.background,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className={classes.root}>
|
||||
<Container>
|
||||
<Group noWrap>
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.2, 1] }}
|
||||
// Spring animation
|
||||
transition={{ duration: 1.5, ease: 'easeInOut' }}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Title variant="gradient" gradient={gradient} className={classes.label}>
|
||||
Homarr v0.11
|
||||
</Title>
|
||||
</motion.div>
|
||||
</Group>
|
||||
<Title mb="md" className={classes.title}>
|
||||
{active === 0 && "Good to see you back! Let's get started"}
|
||||
{active === 1 && progress !== 100 && 'Migrating your configs'}
|
||||
{active === 1 && progress === 100 && 'Migration complete!'}
|
||||
</Title>
|
||||
|
||||
<Stepper active={active}>
|
||||
<Stepper.Step label="Step 1" description="Welcome back">
|
||||
<Text size="lg" align="center" className={classes.description}>
|
||||
<b>A few things have changed since the last time you used Homarr.</b> We'll
|
||||
help you migrate your old configuration to the new format. This process is automatic
|
||||
and should take less than a minute. Then, you'll be able to use the new
|
||||
features of Homarr!
|
||||
</Text>
|
||||
<Alert
|
||||
style={{ maxWidth: 700, margin: 'auto' }}
|
||||
icon={<IconAlertCircle size={16} />}
|
||||
title="Please make a backup of your configs!"
|
||||
color="red"
|
||||
radius="md"
|
||||
variant="outline"
|
||||
>
|
||||
Please make sure to have a backup of your configs in case something goes wrong.{' '}
|
||||
<b>Not all settings can be migrated</b>, so you'll have to re-do some
|
||||
configuration yourself.
|
||||
</Alert>
|
||||
</Stepper.Step>
|
||||
<Stepper.Step
|
||||
loading={progress < 100 && active === 1}
|
||||
icon={progress === 100 && <IconCheck />}
|
||||
label="Step 2"
|
||||
description="Migrating your configs"
|
||||
>
|
||||
<StatsCard configs={configs} progress={progress} setProgress={setProgress} />
|
||||
</Stepper.Step>
|
||||
<Stepper.Step label="Step 3" description="New features">
|
||||
<Text size="lg" align="center" className={classes.description}>
|
||||
<b>Homarr v0.11</b> brings a lot of new features, if you are interested in learning
|
||||
about them, please check out the{' '}
|
||||
<Anchor target="_blank" href="https://homarr.dev/">
|
||||
documentation page
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Stepper.Step>
|
||||
<Stepper.Completed>
|
||||
<Text size="lg" align="center" className={classes.description}>
|
||||
That's it ! We hope you enjoy the new flexibility v0.11 brings. If you spot any
|
||||
bugs make sure to report them as a{' '}
|
||||
<Anchor
|
||||
target="_blank"
|
||||
href="
|
||||
https://github.com/ajnart/homarr/issues/new?assignees=&labels=%F0%9F%90%9B+Bug&template=bug.yml&title=New%20bug"
|
||||
>
|
||||
<b>github issue</b>
|
||||
</Anchor>{' '}
|
||||
or directly on the
|
||||
<Anchor target="_blank" href="https://discord.gg/aCsmEV5RgA">
|
||||
<b>
|
||||
<IconBrandDiscord size={20} />
|
||||
discord !
|
||||
</b>
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Stepper.Completed>
|
||||
</Stepper>
|
||||
<Group position="center" mt="xl">
|
||||
<Button
|
||||
leftIcon={active === 3 && <IconCheck size={20} stroke={1.5} />}
|
||||
onClick={active === 3 ? () => window.location.reload() : nextStep}
|
||||
variant="filled"
|
||||
disabled={active === 1 && progress < 100}
|
||||
>
|
||||
{active === 3 ? 'Finish' : 'Next'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
</div>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
function SwitchToggle() {
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={colorScheme === 'dark'}
|
||||
onChange={() => toggleColorScheme()}
|
||||
size="lg"
|
||||
onLabel={<IconSun color={theme.white} size={20} stroke={1.5} />}
|
||||
offLabel={<IconMoonStars color={theme.colors.gray[6]} size={20} stroke={1.5} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps({ req, res, locale }: GetServerSidePropsContext) {
|
||||
// Get all the configs in the /data/configs folder
|
||||
const configs = await fs.readdirSync('./data/configs');
|
||||
// If there is no config, redirect to the index
|
||||
if (configs.length === 0) {
|
||||
res.writeHead(302, {
|
||||
Location: '/',
|
||||
});
|
||||
res.end();
|
||||
return { props: {} };
|
||||
}
|
||||
// If all the configs are migrated (contains a schemaVersion), redirect to the index
|
||||
if (
|
||||
configs.every(
|
||||
(config) => JSON.parse(fs.readFileSync(`./data/configs/${config}`, 'utf8')).schemaVersion
|
||||
)
|
||||
) {
|
||||
res.writeHead(302, {
|
||||
Location: '/',
|
||||
});
|
||||
res.end();
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale ?? 'en', [])),
|
||||
},
|
||||
};
|
||||
}
|
||||
configs.every((config) => {
|
||||
const configData = JSON.parse(fs.readFileSync(`./data/configs/${config}`, 'utf8'));
|
||||
if (!configData.schemaVersion) {
|
||||
// Migrate the config
|
||||
migrateConfig(configData, config.replace('.json', ''));
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
configs: configs.map(
|
||||
// Get all the file names in ./data/configs
|
||||
(config) => config.replace('.json', '')
|
||||
),
|
||||
|
||||
...(await serverSideTranslations(locale!, [])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ICON_SIZE = 60;
|
||||
|
||||
export function StatsCard({
|
||||
configs,
|
||||
progress,
|
||||
setProgress,
|
||||
}: {
|
||||
configs: string[];
|
||||
progress: number;
|
||||
setProgress: (progress: number) => void;
|
||||
}) {
|
||||
const { classes } = useStyles();
|
||||
const numberOfConfigs = configs.length;
|
||||
// Update the progress every 100ms
|
||||
const [treatedConfigs, setTreatedConfigs] = useState<string[]>([]);
|
||||
// Stop the progress at 100%
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (configs.length === 0) {
|
||||
clearInterval(interval);
|
||||
setProgress(100);
|
||||
return;
|
||||
}
|
||||
// Add last element of configs to the treatedConfigs array
|
||||
setTreatedConfigs((treatedConfigs) => [...treatedConfigs, configs[configs.length - 1]]);
|
||||
// Remove last element of configs
|
||||
configs.pop();
|
||||
}, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
// TODO: Actually use the configs
|
||||
|
||||
return (
|
||||
<Paper radius="md" className={classes.card} mt={ICON_SIZE / 3}>
|
||||
<Group position="apart">
|
||||
<Text size="sm" color="dimmed">
|
||||
Progress
|
||||
</Text>
|
||||
<Text size="sm" color="dimmed">
|
||||
{(100 / (numberOfConfigs + 1)).toFixed(1)}%
|
||||
</Text>
|
||||
</Group>
|
||||
<Stack>
|
||||
<Progress animate={progress < 100} value={100 / (numberOfConfigs + 1)} mt={5} />
|
||||
<List
|
||||
spacing="xs"
|
||||
size="sm"
|
||||
center
|
||||
icon={
|
||||
<ThemeIcon color="teal" size={24} radius="xl">
|
||||
<IconCircleCheck size={16} />
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
{configs.map((config, index) => (
|
||||
<List.Item key={index + 10} icon={<Loader size={24} color="orange" />}>
|
||||
{config ?? 'Unknown'}
|
||||
</List.Item>
|
||||
))}
|
||||
{treatedConfigs.map((config, index) => (
|
||||
<List.Item key={index}>{config ?? 'Unknown'}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Stack>
|
||||
|
||||
<Group position="apart" mt="md">
|
||||
<Space />
|
||||
<Badge size="sm">{configs.length} configs left</Badge>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,10 @@ export const getConfig = (name: string): BackendConfigType => {
|
||||
// Else if config exists but contains no "schema_version" property
|
||||
// then it is an old config file and we should try to migrate it
|
||||
// to the new format.
|
||||
let config = readConfig(name);
|
||||
if (!config.schemaVersion) {
|
||||
// TODO: Migrate config to new format
|
||||
config = migrateConfig(config);
|
||||
const config = readConfig(name);
|
||||
if (config.schemaVersion === undefined) {
|
||||
console.log('Migrating config file...', config);
|
||||
return migrateConfig(config, name);
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BackendConfigType } from '../../types/config';
|
||||
|
||||
export const getFallbackConfig = (name?: string): BackendConfigType => ({
|
||||
schemaVersion: '1.0.0',
|
||||
schemaVersion: 1,
|
||||
configProperties: {
|
||||
name: name ?? 'default',
|
||||
},
|
||||
|
||||
@@ -9,13 +9,14 @@ export const getFrontendConfig = (name: string): ConfigType => {
|
||||
apps: config.apps.map((app) => ({
|
||||
...app,
|
||||
integration: {
|
||||
...app.integration ?? null,
|
||||
...(app.integration ?? null),
|
||||
type: app.integration?.type ?? null,
|
||||
properties: app.integration?.properties.map((property) => ({
|
||||
...property,
|
||||
value: property.type === 'private' ? undefined : property.value,
|
||||
isDefined: property.value != null,
|
||||
})) ?? [],
|
||||
properties:
|
||||
app.integration?.properties.map((property) => ({
|
||||
...property,
|
||||
value: property.type === 'private' ? undefined : property.value,
|
||||
isDefined: property.value != null,
|
||||
})) ?? [],
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import fs from 'fs';
|
||||
import { ConfigType } from '../../types/config';
|
||||
import { Config } from '../types';
|
||||
|
||||
export function migrateConfig(config: Config): ConfigType {
|
||||
export function migrateConfig(config: Config, name: string): ConfigType {
|
||||
const newConfig: ConfigType = {
|
||||
schemaVersion: '1.0.0',
|
||||
schemaVersion: 1,
|
||||
configProperties: {
|
||||
name: config.name ?? 'default',
|
||||
},
|
||||
@@ -76,6 +77,9 @@ export function migrateConfig(config: Config): ConfigType {
|
||||
},
|
||||
},
|
||||
}));
|
||||
// Overrite the file ./data/configs/${name}.json
|
||||
// with the new config format
|
||||
fs.writeFileSync(`./data/configs/${name}.json`, JSON.stringify(newConfig, null, 2));
|
||||
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SettingsType } from './settings';
|
||||
import { IWidget } from '../widgets/widgets';
|
||||
|
||||
export interface ConfigType {
|
||||
schemaVersion: string;
|
||||
schemaVersion: number;
|
||||
configProperties: ConfigPropertiesType;
|
||||
categories: CategoryType[];
|
||||
wrappers: WrapperType[];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createStyles, MantineThemeColors, useMantineTheme } from '@mantine/core';
|
||||
import { Center, createStyles, MantineThemeColors, useMantineTheme } from '@mantine/core';
|
||||
import { Calendar } from '@mantine/dates';
|
||||
import { IconCalendarTime } from '@tabler/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -57,6 +57,8 @@ function CalendarTile({ widget }: CalendarTileProps) {
|
||||
return (
|
||||
<Calendar
|
||||
month={month}
|
||||
// Should be offset 5px to the left
|
||||
style={{ position: 'relative', left: -10, top: -10 }}
|
||||
onMonthChange={setMonth}
|
||||
size="xs"
|
||||
fullWidth
|
||||
|
||||
Reference in New Issue
Block a user