Update default config
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user