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": {
|
"configProperties": {
|
||||||
"name": "default"
|
"name": "default"
|
||||||
},
|
},
|
||||||
"categories": [],
|
"categories": [
|
||||||
|
{
|
||||||
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f",
|
||||||
|
"position": 0,
|
||||||
|
"name": "Example Category"
|
||||||
|
}
|
||||||
|
],
|
||||||
"wrappers": [
|
"wrappers": [
|
||||||
{
|
{
|
||||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e",
|
"id": "default",
|
||||||
"position": 1
|
"position": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"apps": [
|
"apps": [
|
||||||
{
|
{
|
||||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33a",
|
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
|
||||||
"name": "Documentation",
|
"name": "Community",
|
||||||
"url": "https://homarr.dev",
|
"url": "https://discord.com/invite/aCsmEV5RgA",
|
||||||
"behaviour": {
|
"behaviour": {
|
||||||
"onClickUrl": "https://homarr.dev",
|
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
|
||||||
"isOpeningInNewTab": true
|
"isOpeningNewTab": true,
|
||||||
|
"externalUrl": "https://discord.com/invite/aCsmEV5RgA"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"enabledStatusChecker": false,
|
"enabledStatusChecker": false,
|
||||||
@@ -26,7 +33,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"iconUrl": "/imgs/logo/logo.png"
|
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/discord.png"
|
||||||
},
|
},
|
||||||
"integration": {
|
"integration": {
|
||||||
"type": null,
|
"type": null,
|
||||||
@@ -35,7 +42,46 @@
|
|||||||
"area": {
|
"area": {
|
||||||
"type": "wrapper",
|
"type": "wrapper",
|
||||||
"properties": {
|
"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": {
|
"shape": {
|
||||||
@@ -55,7 +101,8 @@
|
|||||||
"url": "https://github.com/ajnart/homarr",
|
"url": "https://github.com/ajnart/homarr",
|
||||||
"behaviour": {
|
"behaviour": {
|
||||||
"onClickUrl": "https://github.com/ajnart/homarr",
|
"onClickUrl": "https://github.com/ajnart/homarr",
|
||||||
"isOpeningInNewTab": true
|
"externalUrl": "https://github.com/ajnart/homarr",
|
||||||
|
"isOpeningNewTab": true
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"enabledStatusChecker": false,
|
"enabledStatusChecker": false,
|
||||||
@@ -73,107 +120,7 @@
|
|||||||
"area": {
|
"area": {
|
||||||
"type": "wrapper",
|
"type": "wrapper",
|
||||||
"properties": {
|
"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-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"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shape": {
|
"shape": {
|
||||||
@@ -181,11 +128,227 @@
|
|||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 3
|
"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": {
|
"size": {
|
||||||
"width": 4,
|
"width": 4,
|
||||||
"height": 2
|
"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": {
|
"settings": {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
|||||||
description={t('general.appname.description')}
|
description={t('general.appname.description')}
|
||||||
placeholder="My example app"
|
placeholder="My example app"
|
||||||
variant="default"
|
variant="default"
|
||||||
mb="md"
|
|
||||||
withAsterisk
|
withAsterisk
|
||||||
required
|
required
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
@@ -45,7 +44,7 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
|
|||||||
description={t('general.externalAddress.description')}
|
description={t('general.externalAddress.description')}
|
||||||
placeholder="https://homarr.mywebsite.com/"
|
placeholder="https://homarr.mywebsite.com/"
|
||||||
variant="default"
|
variant="default"
|
||||||
mb="md"
|
required
|
||||||
{...form.getInputProps('behaviour.externalUrl')}
|
{...form.getInputProps('behaviour.externalUrl')}
|
||||||
/>
|
/>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ContextModalProps } from '@mantine/modals';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Widgets from '../../../../widgets';
|
import Widgets from '../../../../widgets';
|
||||||
|
import type { IWidgetOptionValue } from '../../../../widgets/widgets';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { IWidget } from '../../../../widgets/widgets';
|
import { IWidget } from '../../../../widgets/widgets';
|
||||||
@@ -23,6 +24,8 @@ export const WidgetsEditModal = ({
|
|||||||
const [moduleProperties, setModuleProperties] = useState(innerProps.options);
|
const [moduleProperties, setModuleProperties] = useState(innerProps.options);
|
||||||
const items = Object.entries(moduleProperties ?? {}) as [string, IntegrationOptionsValueType][];
|
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 { name: configName } = useConfigContext();
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||||
|
|
||||||
@@ -63,33 +66,38 @@ export const WidgetsEditModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
{items.map(([key, value]) => (
|
{items.map(([key, value]) => {
|
||||||
<>
|
const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue;
|
||||||
{typeof value === 'boolean' ? (
|
switch (option.type) {
|
||||||
<Switch
|
case 'switch':
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
return (
|
||||||
checked={value}
|
<Switch
|
||||||
onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
|
label={t(`descriptor.settings.${key}.label`)}
|
||||||
/>
|
checked={value as boolean}
|
||||||
) : null}
|
onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
|
||||||
{typeof value === 'string' ? (
|
/>
|
||||||
<TextInput
|
);
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
case 'text':
|
||||||
value={value}
|
return (
|
||||||
onChange={(ev) => handleChange(key, ev.currentTarget.value)}
|
<TextInput
|
||||||
/>
|
label={t(`descriptor.settings.${key}.label`)}
|
||||||
) : null}
|
value={value as string}
|
||||||
{typeof value === 'object' && Array.isArray(value) ? (
|
onChange={(ev) => handleChange(key, ev.currentTarget.value)}
|
||||||
<MultiSelect
|
/>
|
||||||
data={getMutliselectData(key)}
|
);
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
case 'multi-select':
|
||||||
value={value}
|
return (
|
||||||
onChange={(v) => handleChange(key, v)}
|
<MultiSelect
|
||||||
/>
|
data={getMutliselectData(key)}
|
||||||
) : null}
|
label={t(`descriptor.settings.${key}.label`)}
|
||||||
</>
|
value={value as string[]}
|
||||||
))}
|
onChange={(v) => handleChange(key, v)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Button onClick={() => context.closeModal(id)} variant="light">
|
<Button onClick={() => context.closeModal(id)} variant="light">
|
||||||
{t('common:cancel')}
|
{t('common:cancel')}
|
||||||
@@ -99,3 +107,41 @@ export const WidgetsEditModal = ({
|
|||||||
</Stack>
|
</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,
|
IconRowInsertTop,
|
||||||
IconRowInsertBottom,
|
IconRowInsertBottom,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
|
IconTrash,
|
||||||
} from '@tabler/icons';
|
} from '@tabler/icons';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { CategoryType } from '../../../../types/category';
|
import { CategoryType } from '../../../../types/category';
|
||||||
@@ -17,11 +18,11 @@ interface CategoryEditMenuProps {
|
|||||||
|
|
||||||
export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
|
export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
|
||||||
const { name: configName } = useConfigContext();
|
const { name: configName } = useConfigContext();
|
||||||
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit } =
|
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit, remove } =
|
||||||
useCategoryActions(configName, category);
|
useCategoryActions(configName, category);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu withinPortal>
|
<Menu withinPortal position="left-start" withArrow>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<ActionIcon>
|
<ActionIcon>
|
||||||
<IconDots />
|
<IconDots />
|
||||||
@@ -31,6 +32,9 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
|
|||||||
<Menu.Item icon={<IconEdit size={20} />} onClick={edit}>
|
<Menu.Item icon={<IconEdit size={20} />} onClick={edit}>
|
||||||
Edit
|
Edit
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item icon={<IconTrash size={20} />} onClick={remove}>
|
||||||
|
Remove
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Label>Change positon</Menu.Label>
|
<Menu.Label>Change positon</Menu.Label>
|
||||||
<Menu.Item icon={<IconTransitionTop size={20} />} onClick={moveCategoryUp}>
|
<Menu.Item icon={<IconTransitionTop size={20} />} onClick={moveCategoryUp}>
|
||||||
Move up
|
Move up
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const CategoryEditModal = ({
|
|||||||
<TextInput data-autoFocus {...form.getInputProps('name')} label="Name of category" />
|
<TextInput data-autoFocus {...form.getInputProps('name')} label="Name of category" />
|
||||||
|
|
||||||
<Group mt="md" grow>
|
<Group mt="md" grow>
|
||||||
<Button onClick={() => context.closeModal(id)} variant="light" color="gray">
|
<Button onClick={() => context.closeModal(id)} variant="filled" color="gray">
|
||||||
{t('cancel')}
|
{t('cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">{t('save')}</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 () => {
|
const edit = async () => {
|
||||||
openContextModalGeneric<CategoryEditModalInnerProps>({
|
openContextModalGeneric<CategoryEditModalInnerProps>({
|
||||||
modal: 'categoryEditModal',
|
modal: 'categoryEditModal',
|
||||||
@@ -201,6 +232,7 @@ export const useCategoryActions = (configName: string | undefined, category: Cat
|
|||||||
addCategoryBelow,
|
addCategoryBelow,
|
||||||
moveCategoryUp,
|
moveCategoryUp,
|
||||||
moveCategoryDown,
|
moveCategoryDown,
|
||||||
|
remove,
|
||||||
edit,
|
edit,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,10 +15,8 @@ export function Logo({ size = 'md', withoutText = false }: LogoProps) {
|
|||||||
<Group spacing={size === 'md' ? 'xs' : 4} noWrap>
|
<Group spacing={size === 'md' ? 'xs' : 4} noWrap>
|
||||||
<Image
|
<Image
|
||||||
width={size === 'md' ? 50 : 12}
|
width={size === 'md' ? 50 : 12}
|
||||||
src={config?.settings.customization.logoImageUrl || '/imgs/logo/logo.png'}
|
src={config?.settings.customization.logoImageUrl || '/imgs/logo/logo-color.svg'}
|
||||||
style={{
|
alt="Homarr Logo"
|
||||||
position: 'relative',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{withoutText ? null : (
|
{withoutText ? null : (
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { ActionIcon, Button, Popover, Text, Tooltip } from '@mantine/core';
|
import { ActionIcon, Button, Popover, Text, Tooltip } from '@mantine/core';
|
||||||
import { IconEditCircle, IconEditCircleOff, IconX } from '@tabler/icons';
|
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 { 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 { useScreenSmallerThan } from '../../../../../hooks/useScreenSmallerThan';
|
||||||
|
|
||||||
import { useEditModeStore } from '../../../../Dashboard/Views/useEditModeStore';
|
import { useEditModeStore } from '../../../../Dashboard/Views/useEditModeStore';
|
||||||
@@ -13,9 +17,18 @@ export const ToggleEditModeAction = () => {
|
|||||||
const { t } = useTranslation('layout/header/actions/toggle-edit-mode');
|
const { t } = useTranslation('layout/header/actions/toggle-edit-mode');
|
||||||
|
|
||||||
const smallerThanSm = useScreenSmallerThan('sm');
|
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 = () => {
|
const toggleButtonClicked = () => {
|
||||||
toggleEditMode();
|
toggleEditMode();
|
||||||
|
|
||||||
setPopoverManuallyHidden(false);
|
setPopoverManuallyHidden(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Results } from 'sabnzbd-api';
|
import { Results } from 'sabnzbd-api';
|
||||||
import {
|
import { UsenetQueueRequestParams, UsenetQueueResponse } from '../pages/api/modules/usenet/queue';
|
||||||
UsenetQueueRequestParams,
|
|
||||||
UsenetQueueResponse,
|
|
||||||
} from '../pages/api/modules/usenet/queue';
|
|
||||||
import {
|
import {
|
||||||
UsenetHistoryRequestParams,
|
UsenetHistoryRequestParams,
|
||||||
UsenetHistoryResponse,
|
UsenetHistoryResponse,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export function middleware(req: NextRequest, ev: NextFetchEvent) {
|
|||||||
(url.pathname.includes('/_next/') && !url.pathname.includes('/pages/')) ||
|
(url.pathname.includes('/_next/') && !url.pathname.includes('/pages/')) ||
|
||||||
url.pathname === '/favicon.ico' ||
|
url.pathname === '/favicon.ico' ||
|
||||||
url.pathname === '/404' ||
|
url.pathname === '/404' ||
|
||||||
|
url.pathname === '/migrate' ||
|
||||||
url.pathname.includes('pages/_app'));
|
url.pathname.includes('pages/_app'));
|
||||||
if (!skipURL && !isCorrectPassword && process.env.PASSWORD) {
|
if (!skipURL && !isCorrectPassword && process.env.PASSWORD) {
|
||||||
url.pathname = '/login';
|
url.pathname = '/login';
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export async function getServerSideProps({
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
config: {
|
config: {
|
||||||
schemaVersion: '1.0',
|
schemaVersion: 1,
|
||||||
configProperties: {
|
configProperties: {
|
||||||
name: 'Default Configuration',
|
name: 'Default Configuration',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { GetServerSidePropsContext } from 'next';
|
|||||||
import { SSRConfig } from 'next-i18next';
|
import { SSRConfig } from 'next-i18next';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||||
import { Dashboard } from '../components/Dashboard/Dashboard';
|
import { Dashboard } from '../components/Dashboard/Dashboard';
|
||||||
import Layout from '../components/layout/Layout';
|
import Layout from '../components/layout/Layout';
|
||||||
@@ -24,6 +25,24 @@ export async function getServerSideProps({
|
|||||||
res,
|
res,
|
||||||
locale,
|
locale,
|
||||||
}: GetServerSidePropsContext): Promise<{ props: ServerSideProps }> {
|
}: 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 });
|
let configName = getCookie('config-name', { req, res });
|
||||||
const configLocale = getCookie('config-locale', { req, res });
|
const configLocale = getCookie('config-locale', { req, res });
|
||||||
if (!configName) {
|
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
|
// Else if config exists but contains no "schema_version" property
|
||||||
// then it is an old config file and we should try to migrate it
|
// then it is an old config file and we should try to migrate it
|
||||||
// to the new format.
|
// to the new format.
|
||||||
let config = readConfig(name);
|
const config = readConfig(name);
|
||||||
if (!config.schemaVersion) {
|
if (config.schemaVersion === undefined) {
|
||||||
// TODO: Migrate config to new format
|
console.log('Migrating config file...', config);
|
||||||
config = migrateConfig(config);
|
return migrateConfig(config, name);
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BackendConfigType } from '../../types/config';
|
import { BackendConfigType } from '../../types/config';
|
||||||
|
|
||||||
export const getFallbackConfig = (name?: string): BackendConfigType => ({
|
export const getFallbackConfig = (name?: string): BackendConfigType => ({
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: 1,
|
||||||
configProperties: {
|
configProperties: {
|
||||||
name: name ?? 'default',
|
name: name ?? 'default',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ export const getFrontendConfig = (name: string): ConfigType => {
|
|||||||
apps: config.apps.map((app) => ({
|
apps: config.apps.map((app) => ({
|
||||||
...app,
|
...app,
|
||||||
integration: {
|
integration: {
|
||||||
...app.integration ?? null,
|
...(app.integration ?? null),
|
||||||
type: app.integration?.type ?? null,
|
type: app.integration?.type ?? null,
|
||||||
properties: app.integration?.properties.map((property) => ({
|
properties:
|
||||||
...property,
|
app.integration?.properties.map((property) => ({
|
||||||
value: property.type === 'private' ? undefined : property.value,
|
...property,
|
||||||
isDefined: property.value != null,
|
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 { ConfigType } from '../../types/config';
|
||||||
import { Config } from '../types';
|
import { Config } from '../types';
|
||||||
|
|
||||||
export function migrateConfig(config: Config): ConfigType {
|
export function migrateConfig(config: Config, name: string): ConfigType {
|
||||||
const newConfig: ConfigType = {
|
const newConfig: ConfigType = {
|
||||||
schemaVersion: '1.0.0',
|
schemaVersion: 1,
|
||||||
configProperties: {
|
configProperties: {
|
||||||
name: config.name ?? 'default',
|
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;
|
return newConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { SettingsType } from './settings';
|
|||||||
import { IWidget } from '../widgets/widgets';
|
import { IWidget } from '../widgets/widgets';
|
||||||
|
|
||||||
export interface ConfigType {
|
export interface ConfigType {
|
||||||
schemaVersion: string;
|
schemaVersion: number;
|
||||||
configProperties: ConfigPropertiesType;
|
configProperties: ConfigPropertiesType;
|
||||||
categories: CategoryType[];
|
categories: CategoryType[];
|
||||||
wrappers: WrapperType[];
|
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 { Calendar } from '@mantine/dates';
|
||||||
import { IconCalendarTime } from '@tabler/icons';
|
import { IconCalendarTime } from '@tabler/icons';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
@@ -57,6 +57,8 @@ function CalendarTile({ widget }: CalendarTileProps) {
|
|||||||
return (
|
return (
|
||||||
<Calendar
|
<Calendar
|
||||||
month={month}
|
month={month}
|
||||||
|
// Should be offset 5px to the left
|
||||||
|
style={{ position: 'relative', left: -10, top: -10 }}
|
||||||
onMonthChange={setMonth}
|
onMonthChange={setMonth}
|
||||||
size="xs"
|
size="xs"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
Reference in New Issue
Block a user