feat: add server settings (#487)

* feat: add server settings

* feat: remove old migration

* feat: add new migrations

* refactor: format

* fix: build error

* refactor: format

* fix: lint
This commit is contained in:
Manuel
2024-05-19 22:29:15 +02:00
committed by GitHub
parent d9f5158662
commit 919161798e
25 changed files with 2888 additions and 1 deletions

View File

@@ -33,6 +33,7 @@
"@mantine/hooks": "^7.9.2",
"@mantine/modals": "^7.9.2",
"@mantine/tiptap": "^7.9.2",
"@homarr/server-settings": "workspace:^0.1.0",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.37.1",
"@tanstack/react-query-devtools": "^5.37.1",

View File

@@ -14,6 +14,7 @@ import {
IconMailForward,
IconPlug,
IconQuestionMark,
IconSettings,
IconTool,
IconUser,
IconUsers,
@@ -87,6 +88,11 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
},
],
},
{
label: t("items.settings"),
href: "/manage/settings",
icon: IconSettings,
},
{
label: t("items.help.label"),
icon: IconQuestionMark,

View File

@@ -0,0 +1,140 @@
"use client";
import type { ReactNode } from "react";
import React from "react";
import type { MantineSpacing } from "@mantine/core";
import {
Card,
Group,
LoadingOverlay,
Stack,
Switch,
Text,
Title,
UnstyledButton,
} from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import type { UseFormReturnType } from "@homarr/form";
import { useForm } from "@homarr/form";
import type { defaultServerSettings } from "@homarr/server-settings";
import { useScopedI18n } from "@homarr/translation/client";
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
interface AnalyticsSettingsProps {
initialData: typeof defaultServerSettings.analytics;
}
export const AnalyticsSettings = ({ initialData }: AnalyticsSettingsProps) => {
const t = useScopedI18n("management.page.settings.section.analytics");
const form = useForm({
initialValues: initialData,
onValuesChange: (updatedValues, _) => {
if (!form.isValid()) {
return;
}
if (
!updatedValues.enableGeneral &&
(updatedValues.enableWidgetData ||
updatedValues.enableIntegrationData ||
updatedValues.enableUserData)
) {
updatedValues.enableIntegrationData = false;
updatedValues.enableUserData = false;
updatedValues.enableWidgetData = false;
}
void mutateAsync({
settingsKey: "analytics",
value: updatedValues,
});
},
});
const { mutateAsync, isPending } =
clientApi.serverSettings.saveSettings.useMutation({
onSettled: async () => {
await revalidatePathActionAsync("/manage/settings");
},
});
return (
<>
<Title order={2}>{t("title")}</Title>
<Card pos="relative" withBorder>
<LoadingOverlay
visible={isPending}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }}
/>
<Stack>
<SwitchSetting
form={form}
formKey="enableGeneral"
title={t("general.title")}
text={t("general.text")}
/>
<SwitchSetting
form={form}
formKey="enableIntegrationData"
ms="xl"
title={t("integrationData.title")}
text={t("integrationData.text")}
/>
<SwitchSetting
form={form}
formKey="enableWidgetData"
ms="xl"
title={t("widgetData.title")}
text={t("widgetData.text")}
/>
<SwitchSetting
form={form}
formKey="enableUserData"
ms="xl"
title={t("usersData.title")}
text={t("usersData.text")}
/>
</Stack>
</Card>
</>
);
};
const SwitchSetting = ({
form,
ms,
title,
text,
formKey,
}: {
form: UseFormReturnType<typeof defaultServerSettings.analytics>;
formKey: keyof typeof defaultServerSettings.analytics;
ms?: MantineSpacing;
title: string;
text: ReactNode;
}) => {
const handleClick = React.useCallback(() => {
form.setFieldValue(formKey, !form.values[formKey]);
}, [form, formKey]);
return (
<UnstyledButton onClick={handleClick}>
<Group
ms={ms}
justify="space-between"
gap="lg"
align="center"
wrap="nowrap"
>
<Stack gap={0}>
<Text fw="bold">{title}</Text>
<Text c="gray.5">{text}</Text>
</Stack>
<Switch {...form.getInputProps(formKey, { type: "checkbox" })} />
</Group>
</UnstyledButton>
);
};

View File

@@ -0,0 +1,26 @@
import { Stack, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { getScopedI18n } from "@homarr/translation/server";
import { AnalyticsSettings } from "./_components/analytics.settings";
export async function generateMetadata() {
const t = await getScopedI18n("management");
const metaTitle = `${t("metaTitle")} • Homarr`;
return {
title: metaTitle,
};
}
export default async function SettingsPage() {
const serverSettings = await api.serverSettings.getAll();
const t = await getScopedI18n("management.page.settings");
return (
<Stack>
<Title order={1}>{t("title")}</Title>
<AnalyticsSettings initialData={serverSettings.analytics} />
</Stack>
);
}

View File

@@ -22,12 +22,14 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/icons": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.4.5",
"node-cron": "^3.0.3",
"@homarr/icons": "workspace:^0.1.0"
"superjson": "2.2.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -1,3 +1,6 @@
import { jobs } from "./jobs";
import { seedServerSettingsAsync } from "./seed-server-settings";
jobs.startAll();
void seedServerSettingsAsync();

View File

@@ -0,0 +1,33 @@
import SuperJSON from "superjson";
import { db } from "@homarr/db";
import { serverSettings } from "@homarr/db/schema/sqlite";
import { logger } from "@homarr/log";
import {
defaultServerSettings,
defaultServerSettingsKeys,
} from "../../../packages/server-settings";
export const seedServerSettingsAsync = async () => {
const serverSettingsData = await db.query.serverSettings.findMany();
let insertedSettingsCount = 0;
for (const settingsKey of defaultServerSettingsKeys) {
if (
serverSettingsData.some((setting) => setting.settingKey === settingsKey)
) {
return;
}
await db.insert(serverSettings).values({
settingKey: settingsKey,
value: SuperJSON.stringify(defaultServerSettings[settingsKey]),
});
insertedSettingsCount++;
}
if (insertedSettingsCount > 0) {
logger.info(`Inserted ${insertedSettingsCount} missing settings`);
}
};