diff --git a/public/locales/en/settings/general/config-changer.json b/public/locales/en/settings/general/config-changer.json
index 130273158..2ca62d86f 100644
--- a/public/locales/en/settings/general/config-changer.json
+++ b/public/locales/en/settings/general/config-changer.json
@@ -57,9 +57,11 @@
}
},
"accept": {
+ "title": "Configuration Upload",
"text": "Drag files here to upload a config. Support for JSON only."
},
"reject": {
+ "title": "Drag and Drop Upload rejected",
"text": "This file format is not supported. Please only upload JSON."
}
}
diff --git a/src/components/Config/LoadConfig.tsx b/src/components/Config/LoadConfig.tsx
index 47c4bbef4..e653a19ba 100644
--- a/src/components/Config/LoadConfig.tsx
+++ b/src/components/Config/LoadConfig.tsx
@@ -1,77 +1,94 @@
-import { Group, Text, useMantineTheme } from '@mantine/core';
+import { Group, Stack, Text, Title, useMantineTheme } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { showNotification } from '@mantine/notifications';
import { IconCheck as Check, IconPhoto, IconUpload, IconX, IconX as X } from '@tabler/icons';
import { setCookie } from 'cookies-next';
import { useTranslation } from 'next-i18next';
import { useConfigStore } from '../../config/store';
+import { migrateConfig } from '../../tools/config/migrateConfig';
import { Config } from '../../tools/types';
+import { ConfigType } from '../../types/config';
-export default function LoadConfigComponent(props: any) {
- const { updateConfig } = useConfigStore();
+export const LoadConfigComponent = () => {
+ const { addConfig } = useConfigStore();
const theme = useMantineTheme();
const { t } = useTranslation('settings/general/config-changer');
return (
{
- files[0].text().then((e) => {
- try {
- JSON.parse(e) as Config;
- } catch (e) {
- showNotification({
- autoClose: 5000,
- title: {t('dropzone.notifications.invalidConfig.title')},
- color: 'red',
- icon: ,
- message: t('dropzone.notifications.invalidConfig.message'),
- });
- return;
- }
- const newConfig: Config = JSON.parse(e);
+ onDrop={async (files) => {
+ const fileName = files[0].name.replaceAll('.json', '');
+ const fileText = await files[0].text();
+
+ try {
+ JSON.parse(fileText) as ConfigType;
+ } catch (e) {
showNotification({
autoClose: 5000,
- radius: 'md',
- title: (
-
- {t('dropzone.notifications.loadedSuccessfully.title', {
- configName: newConfig.name,
- })}
-
- ),
- color: 'green',
- icon: ,
- message: undefined,
+ title: {t('dropzone.notifications.invalidConfig.title')},
+ color: 'red',
+ icon: ,
+ message: t('dropzone.notifications.invalidConfig.message'),
});
- setCookie('config-name', newConfig.name, {
- maxAge: 60 * 60 * 24 * 30,
- sameSite: 'strict',
- });
- updateConfig(newConfig.name, (previousConfig) => ({ ...previousConfig, newConfig }));
+ return;
+ }
+
+ let newConfig: ConfigType = JSON.parse(fileText);
+
+ if (!newConfig.schemaVersion) {
+ console.warn('a legacy configuration schema was deteced and migrated to the current schema');
+ const oldConfig = JSON.parse(fileText) as Config;
+ newConfig = migrateConfig(oldConfig);
+ }
+
+ await addConfig(fileName, newConfig, true);
+ showNotification({
+ autoClose: 5000,
+ radius: 'md',
+ title: (
+
+ {t('dropzone.notifications.loadedSuccessfully.title', {
+ configName: fileName,
+ })}
+
+ ),
+ color: 'green',
+ icon: ,
+ message: undefined,
+ });
+ setCookie('config-name', fileName, {
+ maxAge: 60 * 60 * 24 * 30,
+ sameSite: 'strict',
});
}}
accept={['application/json']}
>
-
+
- {t('dropzone.accept.text')}
-
+ {t('dropzone.accept.title')}
+
+ {t('dropzone.accept.text')}
+
+
-
+
- {t('dropzone.reject.text')}
-
+ {t('dropzone.reject.title')}
+
+ {t('dropzone.reject.text')}
+
+
@@ -79,4 +96,4 @@ export default function LoadConfigComponent(props: any) {
);
-}
+};
diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx
index 6872f6966..6385a1d76 100644
--- a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx
+++ b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx
@@ -32,8 +32,6 @@ export const WidgetsEditModal = ({
if (!configName || !innerProps.options) return null;
- console.log(`loaded namespace modules/${innerProps.widgetId}`);
-
const handleChange = (key: string, value: IntegrationOptionsValueType) => {
setModuleProperties((prev) => {
const copyOfPrev: any = { ...prev };
diff --git a/src/config/store.ts b/src/config/store.ts
index a2842214f..0839aee60 100644
--- a/src/config/store.ts
+++ b/src/config/store.ts
@@ -13,6 +13,20 @@ export const useConfigStore = create((set, get) => ({
],
}));
},
+ addConfig: async (name: string, config: ConfigType, shouldSaveConfigToFileSystem = true) => {
+ set((old) => ({
+ ...old,
+ configs: [
+ ...old.configs.filter((x) => x.value.configProperties.name !== name),
+ { value: config, increaseVersion: () => {} },
+ ],
+ }));
+
+ if (!shouldSaveConfigToFileSystem) {
+ return;
+ }
+ axios.put(`/api/configs/${name}`, { ...config });
+ },
updateConfig: async (
name,
updateCallback: (previous: ConfigType) => ConfigType,
@@ -21,7 +35,9 @@ export const useConfigStore = create((set, get) => ({
) => {
const { configs } = get();
const currentConfig = configs.find((x) => x.value.configProperties.name === name);
- if (!currentConfig) return;
+ if (!currentConfig) {
+ return;
+ }
// copies the value of currentConfig and creates a non reference object named previousConfig
const previousConfig: ConfigType = JSON.parse(JSON.stringify(currentConfig.value));
@@ -51,6 +67,11 @@ export const useConfigStore = create((set, get) => ({
interface UseConfigStoreType {
configs: { increaseVersion: () => void; value: ConfigType }[];
initConfig: (name: string, config: ConfigType, increaseVersion: () => void) => void;
+ addConfig: (
+ name: string,
+ config: ConfigType,
+ shouldSaveConfigToFileSystem: boolean,
+ ) => Promise;
updateConfig: (
name: string,
updateCallback: (previous: ConfigType) => ConfigType,
diff --git a/src/pages/[slug].tsx b/src/pages/[slug].tsx
index 29578cdf3..0878371c3 100644
--- a/src/pages/[slug].tsx
+++ b/src/pages/[slug].tsx
@@ -1,7 +1,7 @@
import fs from 'fs';
import { GetServerSidePropsContext } from 'next';
import path from 'path';
-import LoadConfigComponent from '../components/Config/LoadConfig';
+import { LoadConfigComponent } from '../components/Config/LoadConfig';
import { Dashboard } from '../components/Dashboard/Dashboard';
import Layout from '../components/layout/Layout';
import { useInitConfig } from '../config/init';
diff --git a/src/pages/api/configs/[slug].ts b/src/pages/api/configs/[slug].ts
index 68f1cc625..3d1ea5605 100644
--- a/src/pages/api/configs/[slug].ts
+++ b/src/pages/api/configs/[slug].ts
@@ -1,6 +1,7 @@
-import { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs';
import path from 'path';
+import Consola from 'consola';
+import { NextApiRequest, NextApiResponse } from 'next';
import { BackendConfigType, ConfigType } from '../../../types/config';
import { getConfig } from '../../../tools/config/getConfig';
@@ -10,11 +11,14 @@ function Put(req: NextApiRequest, res: NextApiResponse) {
// Get the body of the request
const { body: config }: { body: ConfigType } = req;
if (!slug || !config) {
+ Consola.warn('Rejected configuration update because either config or slug were undefined');
return res.status(400).json({
error: 'Wrong request',
});
}
+ Consola.info(`Saving updated configuration of '${slug}' config.`);
+
const previousConfig = getConfig(slug);
const newConfig: BackendConfigType = {
@@ -56,11 +60,11 @@ function Put(req: NextApiRequest, res: NextApiResponse) {
};
// Save the body in the /data/config folder with the slug as filename
- fs.writeFileSync(
- path.join('data/configs', `${slug}.json`),
- JSON.stringify(newConfig, null, 2),
- 'utf8'
- );
+ const targetPath = path.join('data/configs', `${slug}.json`);
+ fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');
+
+ Consola.info(`Config '${slug}' has been updated and flushed to '${targetPath}'.`);
+
return res.status(200).json({
message: 'Configuration saved with success',
});
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index ae65d9961..d68cf541c 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -2,7 +2,7 @@ import { getCookie, setCookie } from 'cookies-next';
import { GetServerSidePropsContext } from 'next';
import fs from 'fs';
-import LoadConfigComponent from '../components/Config/LoadConfig';
+import Consola from 'consola';
import { Dashboard } from '../components/Dashboard/Dashboard';
import Layout from '../components/layout/Layout';
import { useInitConfig } from '../config/init';
@@ -10,6 +10,7 @@ import { getFrontendConfig } from '../tools/config/getFrontendConfig';
import { getServerSideTranslations } from '../tools/getServerSideTranslations';
import { dashboardNamespaces } from '../tools/translation-namespaces';
import { DashboardServerSideProps } from '../types/dashboardPageType';
+import { LoadConfigComponent } from '../components/Config/LoadConfig';
export async function getServerSideProps({
req,
@@ -46,6 +47,9 @@ export async function getServerSideProps({
}
const translations = await getServerSideTranslations(req, res, dashboardNamespaces, locale);
+
+ Consola.info(`Decided to use configuration '${configName}'`);
+
const config = getFrontendConfig(configName as string);
return {
diff --git a/src/pages/migrate.tsx b/src/pages/migrate.tsx
index 1fa14e5fc..c603d903c 100644
--- a/src/pages/migrate.tsx
+++ b/src/pages/migrate.tsx
@@ -1,45 +1,45 @@
-import React, { useEffect, useState } from 'react';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import fs from 'fs';
import { GetServerSidePropsContext } from 'next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import React, { useEffect, useState } from 'react';
import {
- createStyles,
- Title,
- Text,
- Container,
- Group,
- Stepper,
- useMantineTheme,
- Header,
+ Alert,
+ Anchor,
AppShell,
- useMantineColorScheme,
- Switch,
+ Badge,
Box,
Button,
- Alert,
- Badge,
+ Container,
+ createStyles,
+ Group,
+ Header,
List,
Loader,
Paper,
Progress,
Space,
Stack,
+ Stepper,
+ Switch,
+ Text,
ThemeIcon,
- Anchor,
+ Title,
+ useMantineColorScheme,
+ useMantineTheme,
} from '@mantine/core';
import {
- IconSun,
- IconMoonStars,
- IconCheck,
IconAlertCircle,
- IconCircleCheck,
IconBrandDiscord,
+ IconCheck,
+ IconCircleCheck,
+ IconMoonStars,
+ IconSun,
} 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';
+import { backendMigrateConfig } from '../tools/config/backendMigrateConfig';
const useStyles = createStyles((theme) => ({
root: {
@@ -274,7 +274,7 @@ export async function getServerSideProps({ req, res, locale }: GetServerSideProp
const configData = JSON.parse(fs.readFileSync(`./data/configs/${config}`, 'utf8'));
if (!configData.schemaVersion) {
// Migrate the config
- migrateConfig(configData, config.replace('.json', ''));
+ backendMigrateConfig(configData, config.replace('.json', ''));
}
return config;
});
diff --git a/src/tools/config/backendMigrateConfig.ts b/src/tools/config/backendMigrateConfig.ts
new file mode 100644
index 000000000..54bb55693
--- /dev/null
+++ b/src/tools/config/backendMigrateConfig.ts
@@ -0,0 +1,14 @@
+import fs from 'fs';
+import { ConfigType } from '../../types/config';
+import { Config } from '../types';
+import { migrateConfig } from './migrateConfig';
+
+export function backendMigrateConfig(config: Config, name: string): ConfigType {
+ const migratedConfig = migrateConfig(config);
+
+ // Overrite the file ./data/configs/${name}.json
+ // with the new config format
+ fs.writeFileSync(`./data/configs/${name}.json`, JSON.stringify(migratedConfig, null, 2));
+
+ return migratedConfig;
+}
diff --git a/src/tools/config/migrateConfig.ts b/src/tools/config/migrateConfig.ts
index 6385da4af..9648c1fd1 100644
--- a/src/tools/config/migrateConfig.ts
+++ b/src/tools/config/migrateConfig.ts
@@ -1,8 +1,11 @@
-import fs from 'fs';
+import { v4 as uuidv4 } from 'uuid';
+import { AppType } from '../../types/app';
+import { AreaType } from '../../types/area';
+import { CategoryType } from '../../types/category';
import { ConfigType } from '../../types/config';
-import { Config } from '../types';
+import { Config, serviceItem } from '../types';
-export function migrateConfig(config: Config, name: string): ConfigType {
+export function migrateConfig(config: Config): ConfigType {
const newConfig: ConfigType = {
schemaVersion: 1,
configProperties: {
@@ -41,45 +44,86 @@ export function migrateConfig(config: Config, name: string): ConfigType {
],
};
- newConfig.apps = config.services.map((s, idx) => ({
- name: s.name,
- id: s.id,
- url: s.url,
- behaviour: {
- isOpeningNewTab: s.newTab ?? true,
- externalUrl: s.openedUrl ?? '',
- },
- network: {
- enabledStatusChecker: s.ping ?? true,
- okStatus: s.status?.map((str) => parseInt(str, 10)) ?? [200],
- },
- appearance: {
- iconUrl: s.icon,
- },
- integration: {
- type: null,
- properties: [],
- },
- area: {
- type: 'wrapper',
- properties: {
- id: 'default',
- },
- },
- shape: {
- location: {
- x: (idx * 3) % 18,
- y: Math.floor(idx / 6) * 3,
- },
- size: {
- width: 3,
- height: 3,
- },
- },
- }));
- // Overrite the file ./data/configs/${name}.json
- // with the new config format
- fs.writeFileSync(`./data/configs/${name}.json`, JSON.stringify(newConfig, null, 2));
+ config.services.forEach((service, index) => {
+ const { category: categoryName } = service;
+
+ if (!categoryName) {
+ newConfig.apps.push(
+ migrateService(service, index, {
+ type: 'wrapper',
+ properties: {
+ id: 'default',
+ },
+ })
+ );
+ return;
+ }
+
+ const category = getConfigAndCreateIfNotExsists(newConfig, categoryName);
+
+ if (!category) {
+ return;
+ }
+
+ newConfig.apps.push(
+ migrateService(service, index, { type: 'category', properties: { id: category.id } })
+ );
+ });
return newConfig;
}
+
+const getConfigAndCreateIfNotExsists = (
+ config: ConfigType,
+ categoryName: string
+): CategoryType | null => {
+ const foundCategory = config.categories.find((c) => c.name === categoryName);
+ if (foundCategory) {
+ return foundCategory;
+ }
+
+ const category: CategoryType = {
+ id: uuidv4(),
+ name: categoryName,
+ position: 0,
+ };
+
+ config.categories.push(category);
+ return category;
+};
+
+const migrateService = (
+ oldService: serviceItem,
+ serviceIndex: number,
+ areaType: AreaType
+): AppType => ({
+ id: uuidv4(),
+ name: oldService.name,
+ url: oldService.url,
+ behaviour: {
+ isOpeningNewTab: oldService.newTab ?? true,
+ externalUrl: oldService.openedUrl ?? '',
+ },
+ network: {
+ enabledStatusChecker: oldService.ping ?? true,
+ okStatus: oldService.status?.map((str) => parseInt(str, 10)) ?? [200],
+ },
+ appearance: {
+ iconUrl: oldService.icon,
+ },
+ integration: {
+ type: null,
+ properties: [],
+ },
+ area: areaType,
+ shape: {
+ location: {
+ x: (serviceIndex * 3) % 18,
+ y: Math.floor(serviceIndex / 6) * 3,
+ },
+ size: {
+ width: 3,
+ height: 3,
+ },
+ },
+});