diff --git a/.env b/.env new file mode 100644 index 000000000..4b400d5e7 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PASSWORD=TOTOISCOOL \ No newline at end of file diff --git a/data/configs/default.json b/data/configs/default.json index 7d7dc6277..2b722842e 100644 --- a/data/configs/default.json +++ b/data/configs/default.json @@ -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": { diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/GeneralTab/GeneralTab.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/GeneralTab/GeneralTab.tsx index 0cf437e02..56a4e5e54 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/GeneralTab/GeneralTab.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/GeneralTab/GeneralTab.tsx @@ -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')} /> diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx index caae1b41b..70ef85a66 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx @@ -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 ( - {items.map(([key, value]) => ( - <> - {typeof value === 'boolean' ? ( - handleChange(key, ev.currentTarget.checked)} - /> - ) : null} - {typeof value === 'string' ? ( - handleChange(key, ev.currentTarget.value)} - /> - ) : null} - {typeof value === 'object' && Array.isArray(value) ? ( - handleChange(key, v)} - /> - ) : null} - - ))} - + {items.map(([key, value]) => { + const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue; + switch (option.type) { + case 'switch': + return ( + handleChange(key, ev.currentTarget.checked)} + /> + ); + case 'text': + return ( + handleChange(key, ev.currentTarget.value)} + /> + ); + case 'multi-select': + return ( + handleChange(key, v)} + /> + ); + default: + return null; + } + })} +// +// +// +// ); +// }; diff --git a/src/components/Dashboard/Wrappers/Category/CategoryEditMenu.tsx b/src/components/Dashboard/Wrappers/Category/CategoryEditMenu.tsx index b49fb4cde..24c89bc8f 100644 --- a/src/components/Dashboard/Wrappers/Category/CategoryEditMenu.tsx +++ b/src/components/Dashboard/Wrappers/Category/CategoryEditMenu.tsx @@ -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 ( - + @@ -31,6 +32,9 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => { } onClick={edit}> Edit + } onClick={remove}> + Remove + Change positon } onClick={moveCategoryUp}> Move up diff --git a/src/components/Dashboard/Wrappers/Category/CategoryEditModal.tsx b/src/components/Dashboard/Wrappers/Category/CategoryEditModal.tsx index 1a80f63b7..18a2b6d19 100644 --- a/src/components/Dashboard/Wrappers/Category/CategoryEditModal.tsx +++ b/src/components/Dashboard/Wrappers/Category/CategoryEditModal.tsx @@ -39,7 +39,7 @@ export const CategoryEditModal = ({ - diff --git a/src/components/Dashboard/Wrappers/Category/useCategoryActions.tsx b/src/components/Dashboard/Wrappers/Category/useCategoryActions.tsx index 3c1452da6..abd1496ec 100644 --- a/src/components/Dashboard/Wrappers/Category/useCategoryActions.tsx +++ b/src/components/Dashboard/Wrappers/Category/useCategoryActions.tsx @@ -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({ modal: 'categoryEditModal', @@ -201,6 +232,7 @@ export const useCategoryActions = (configName: string | undefined, category: Cat addCategoryBelow, moveCategoryUp, moveCategoryDown, + remove, edit, }; }; diff --git a/src/components/layout/Logo.tsx b/src/components/layout/Logo.tsx index ef943ca43..a943f5866 100644 --- a/src/components/layout/Logo.tsx +++ b/src/components/layout/Logo.tsx @@ -15,10 +15,8 @@ export function Logo({ size = 'md', withoutText = false }: LogoProps) { Homarr Logo {withoutText ? null : ( { 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); }; diff --git a/src/hooks/api.ts b/src/hooks/api.ts index 77f5dba21..a50119cb8 100644 --- a/src/hooks/api.ts +++ b/src/hooks/api.ts @@ -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, diff --git a/src/middleware.ts b/src/middleware.ts index 9c45b2840..15b770e8b 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -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'; diff --git a/src/pages/[slug].tsx b/src/pages/[slug].tsx index d8bbecde6..02591dabb 100644 --- a/src/pages/[slug].tsx +++ b/src/pages/[slug].tsx @@ -33,7 +33,7 @@ export async function getServerSideProps({ return { props: { config: { - schemaVersion: '1.0', + schemaVersion: 1, configProperties: { name: 'Default Configuration', }, diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 9aac2d258..c4ab22ef1 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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) { diff --git a/src/pages/migrate.tsx b/src/pages/migrate.tsx new file mode 100644 index 000000000..1fa14e5fc --- /dev/null +++ b/src/pages/migrate.tsx @@ -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 ( + ({ + main: { + backgroundColor: + theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0], + }, + })} + > + + + + + + + {/* Header content */} + + } + styles={(theme) => ({ + main: { + backgroundColor: theme.fn.variant({ variant: 'light', color: theme.primaryColor }) + .background, + }, + })} + > +
+ + + + + Homarr v0.11 + + + + + {active === 0 && "Good to see you back! Let's get started"} + {active === 1 && progress !== 100 && 'Migrating your configs'} + {active === 1 && progress === 100 && 'Migration complete!'} + + + + + + A few things have changed since the last time you used Homarr. 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! + + } + 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.{' '} + Not all settings can be migrated, so you'll have to re-do some + configuration yourself. + + + } + label="Step 2" + description="Migrating your configs" + > + + + + + Homarr v0.11 brings a lot of new features, if you are interested in learning + about them, please check out the{' '} + + documentation page + + + + + + 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{' '} + + github issue + {' '} + or directly on the + + + + discord ! + + + + + + + + + +
+
+ ); +} + +function SwitchToggle() { + const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + const theme = useMantineTheme(); + + return ( + toggleColorScheme()} + size="lg" + onLabel={} + offLabel={} + /> + ); +} + +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([]); + // 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 ( + + + + Progress + + + {(100 / (numberOfConfigs + 1)).toFixed(1)}% + + + + + + + + } + > + {configs.map((config, index) => ( + }> + {config ?? 'Unknown'} + + ))} + {treatedConfigs.map((config, index) => ( + {config ?? 'Unknown'} + ))} + + + + + + {configs.length} configs left + + + ); +} diff --git a/src/tools/config/getConfig.ts b/src/tools/config/getConfig.ts index a59cefb97..7d4e29d50 100644 --- a/src/tools/config/getConfig.ts +++ b/src/tools/config/getConfig.ts @@ -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; }; diff --git a/src/tools/config/getFallbackConfig.ts b/src/tools/config/getFallbackConfig.ts index f452c0cca..922362bc3 100644 --- a/src/tools/config/getFallbackConfig.ts +++ b/src/tools/config/getFallbackConfig.ts @@ -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', }, diff --git a/src/tools/config/getFrontendConfig.ts b/src/tools/config/getFrontendConfig.ts index 4b580bd40..118249057 100644 --- a/src/tools/config/getFrontendConfig.ts +++ b/src/tools/config/getFrontendConfig.ts @@ -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, + })) ?? [], }, })), }; diff --git a/src/tools/config/migrateConfig.ts b/src/tools/config/migrateConfig.ts index 230d47378..6385da4af 100644 --- a/src/tools/config/migrateConfig.ts +++ b/src/tools/config/migrateConfig.ts @@ -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; } diff --git a/src/types/config.ts b/src/types/config.ts index 293fdd9e4..53a8a3f73 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -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[]; diff --git a/src/widgets/calendar/CalendarTile.tsx b/src/widgets/calendar/CalendarTile.tsx index d326b7951..22c6cd480 100644 --- a/src/widgets/calendar/CalendarTile.tsx +++ b/src/widgets/calendar/CalendarTile.tsx @@ -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 (