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:
@@ -33,6 +33,7 @@
|
|||||||
"@mantine/hooks": "^7.9.2",
|
"@mantine/hooks": "^7.9.2",
|
||||||
"@mantine/modals": "^7.9.2",
|
"@mantine/modals": "^7.9.2",
|
||||||
"@mantine/tiptap": "^7.9.2",
|
"@mantine/tiptap": "^7.9.2",
|
||||||
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@t3-oss/env-nextjs": "^0.10.1",
|
"@t3-oss/env-nextjs": "^0.10.1",
|
||||||
"@tanstack/react-query": "^5.37.1",
|
"@tanstack/react-query": "^5.37.1",
|
||||||
"@tanstack/react-query-devtools": "^5.37.1",
|
"@tanstack/react-query-devtools": "^5.37.1",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
IconMailForward,
|
IconMailForward,
|
||||||
IconPlug,
|
IconPlug,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
|
IconSettings,
|
||||||
IconTool,
|
IconTool,
|
||||||
IconUser,
|
IconUser,
|
||||||
IconUsers,
|
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"),
|
label: t("items.help.label"),
|
||||||
icon: IconQuestionMark,
|
icon: IconQuestionMark,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
26
apps/nextjs/src/app/[locale]/manage/settings/page.tsx
Normal file
26
apps/nextjs/src/app/[locale]/manage/settings/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,12 +22,14 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
|
"@homarr/icons": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"@homarr/icons": "workspace:^0.1.0"
|
"superjson": "2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
import { jobs } from "./jobs";
|
import { jobs } from "./jobs";
|
||||||
|
import { seedServerSettingsAsync } from "./seed-server-settings";
|
||||||
|
|
||||||
jobs.startAll();
|
jobs.startAll();
|
||||||
|
|
||||||
|
void seedServerSettingsAsync();
|
||||||
|
|||||||
33
apps/tasks/src/seed-server-settings.ts
Normal file
33
apps/tasks/src/seed-server-settings.ts
Normal 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`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/tasks": "workspace:^0.1.0",
|
"@homarr/tasks": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@trpc/client": "next",
|
"@trpc/client": "next",
|
||||||
"@trpc/server": "next",
|
"@trpc/server": "next",
|
||||||
"superjson": "2.2.1"
|
"superjson": "2.2.1"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { integrationRouter } from "./router/integration";
|
|||||||
import { inviteRouter } from "./router/invite";
|
import { inviteRouter } from "./router/invite";
|
||||||
import { locationRouter } from "./router/location";
|
import { locationRouter } from "./router/location";
|
||||||
import { logRouter } from "./router/log";
|
import { logRouter } from "./router/log";
|
||||||
|
import { serverSettingsRouter } from "./router/serverSettings";
|
||||||
import { userRouter } from "./router/user";
|
import { userRouter } from "./router/user";
|
||||||
import { widgetRouter } from "./router/widgets";
|
import { widgetRouter } from "./router/widgets";
|
||||||
import { createTRPCRouter } from "./trpc";
|
import { createTRPCRouter } from "./trpc";
|
||||||
@@ -23,6 +24,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
log: logRouter,
|
log: logRouter,
|
||||||
icon: iconsRouter,
|
icon: iconsRouter,
|
||||||
home: homeRouter,
|
home: homeRouter,
|
||||||
|
serverSettings: serverSettingsRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
43
packages/api/src/router/serverSettings.ts
Normal file
43
packages/api/src/router/serverSettings.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
|
import { eq } from "@homarr/db";
|
||||||
|
import { serverSettings } from "@homarr/db/schema/sqlite";
|
||||||
|
import type { ServerSettings } from "@homarr/server-settings";
|
||||||
|
import { defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||||
|
import { z } from "@homarr/validation";
|
||||||
|
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
|
export const serverSettingsRouter = createTRPCRouter({
|
||||||
|
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const settings = await ctx.db.query.serverSettings.findMany();
|
||||||
|
|
||||||
|
const data = {} as ServerSettings;
|
||||||
|
defaultServerSettingsKeys.forEach((key) => {
|
||||||
|
const settingValue = settings.find(
|
||||||
|
(setting) => setting.settingKey === key,
|
||||||
|
)?.value;
|
||||||
|
if (!settingValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data[key] = SuperJSON.parse(settingValue);
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
saveSettings: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
settingsKey: z.enum(defaultServerSettingsKeys),
|
||||||
|
value: z.record(z.string(), z.unknown()),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const databaseRunResult = await ctx.db
|
||||||
|
.update(serverSettings)
|
||||||
|
.set({
|
||||||
|
value: SuperJSON.stringify(input.value),
|
||||||
|
})
|
||||||
|
.where(eq(serverSettings.settingKey, input.settingsKey));
|
||||||
|
return databaseRunResult.changes === 1;
|
||||||
|
}),
|
||||||
|
});
|
||||||
134
packages/api/src/router/test/serverSettings.spec.ts
Normal file
134
packages/api/src/router/test/serverSettings.spec.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import SuperJSON from "superjson";
|
||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { Session } from "@homarr/auth";
|
||||||
|
import { createId } from "@homarr/db";
|
||||||
|
import { serverSettings } from "@homarr/db/schema/sqlite";
|
||||||
|
import { createDb } from "@homarr/db/test";
|
||||||
|
import {
|
||||||
|
defaultServerSettings,
|
||||||
|
defaultServerSettingsKeys,
|
||||||
|
} from "@homarr/server-settings";
|
||||||
|
|
||||||
|
import { serverSettingsRouter } from "../serverSettings";
|
||||||
|
|
||||||
|
// Mock the auth module to return an empty session
|
||||||
|
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
|
||||||
|
|
||||||
|
const defaultSession = {
|
||||||
|
user: {
|
||||||
|
id: createId(),
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
expires: new Date().toISOString(),
|
||||||
|
} satisfies Session;
|
||||||
|
|
||||||
|
describe("getAll server settings", () => {
|
||||||
|
test("getAll should throw error when unauthorized", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = serverSettingsRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(serverSettings).values([
|
||||||
|
{
|
||||||
|
settingKey: defaultServerSettingsKeys[0],
|
||||||
|
value: SuperJSON.stringify(defaultServerSettings.analytics),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const actAsync = async () => await caller.getAll();
|
||||||
|
|
||||||
|
await expect(actAsync()).rejects.toThrow();
|
||||||
|
});
|
||||||
|
test("getAll should return server", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = serverSettingsRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: defaultSession,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(serverSettings).values([
|
||||||
|
{
|
||||||
|
settingKey: defaultServerSettingsKeys[0],
|
||||||
|
value: SuperJSON.stringify(defaultServerSettings.analytics),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await caller.getAll();
|
||||||
|
|
||||||
|
expect(result).toStrictEqual({
|
||||||
|
analytics: {
|
||||||
|
enableGeneral: true,
|
||||||
|
enableWidgetData: false,
|
||||||
|
enableIntegrationData: false,
|
||||||
|
enableUserData: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("saveSettings", () => {
|
||||||
|
test("saveSettings should return false when it did not update one", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = serverSettingsRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: defaultSession,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await caller.saveSettings({
|
||||||
|
settingsKey: "analytics",
|
||||||
|
value: {
|
||||||
|
enableGeneral: true,
|
||||||
|
enableWidgetData: true,
|
||||||
|
enableIntegrationData: true,
|
||||||
|
enableUserData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
|
||||||
|
const dbSettings = await db.select().from(serverSettings);
|
||||||
|
expect(dbSettings.length).toBe(0);
|
||||||
|
});
|
||||||
|
test("saveSettings should update settings and return true when it updated only one", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = serverSettingsRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: defaultSession,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(serverSettings).values([
|
||||||
|
{
|
||||||
|
settingKey: defaultServerSettingsKeys[0],
|
||||||
|
value: SuperJSON.stringify(defaultServerSettings.analytics),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await caller.saveSettings({
|
||||||
|
settingsKey: "analytics",
|
||||||
|
value: {
|
||||||
|
enableGeneral: true,
|
||||||
|
enableWidgetData: true,
|
||||||
|
enableIntegrationData: true,
|
||||||
|
enableUserData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
const dbSettings = await db.select().from(serverSettings);
|
||||||
|
expect(dbSettings).toStrictEqual([
|
||||||
|
{
|
||||||
|
settingKey: "analytics",
|
||||||
|
value: SuperJSON.stringify({
|
||||||
|
enableGeneral: true,
|
||||||
|
enableWidgetData: true,
|
||||||
|
enableIntegrationData: true,
|
||||||
|
enableUserData: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE `serverSetting` (
|
||||||
|
`key` varchar(64) NOT NULL,
|
||||||
|
`value` text NOT NULL DEFAULT ('{"json": {}}'),
|
||||||
|
CONSTRAINT `serverSetting_key` PRIMARY KEY(`key`),
|
||||||
|
CONSTRAINT `serverSetting_key_unique` UNIQUE(`key`)
|
||||||
|
);
|
||||||
1200
packages/db/migrations/mysql/meta/0002_snapshot.json
Normal file
1200
packages/db/migrations/mysql/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,13 @@
|
|||||||
"when": 1715885855801,
|
"when": 1715885855801,
|
||||||
"tag": "0001_wild_alex_wilder",
|
"tag": "0001_wild_alex_wilder",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1716148439439,
|
||||||
|
"tag": "0002_freezing_black_panther",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
6
packages/db/migrations/sqlite/0002_adorable_raider.sql
Normal file
6
packages/db/migrations/sqlite/0002_adorable_raider.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE `serverSetting` (
|
||||||
|
`key` text PRIMARY KEY NOT NULL,
|
||||||
|
`value` text DEFAULT '{"json": {}}' NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `serverSetting_key_unique` ON `serverSetting` (`key`);
|
||||||
1144
packages/db/migrations/sqlite/meta/0002_snapshot.json
Normal file
1144
packages/db/migrations/sqlite/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,13 @@
|
|||||||
"when": 1715871797713,
|
"when": 1715871797713,
|
||||||
"tag": "0001_mixed_titanium_man",
|
"tag": "0001_mixed_titanium_man",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1716148434186,
|
||||||
|
"tag": "0002_adorable_raider",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,6 +311,11 @@ export const iconRepositories = mysqlTable("iconRepository", {
|
|||||||
slug: varchar("iconRepository_slug", { length: 150 }).notNull(),
|
slug: varchar("iconRepository_slug", { length: 150 }).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const serverSettings = mysqlTable("serverSetting", {
|
||||||
|
settingKey: varchar("key", { length: 64 }).notNull().unique().primaryKey(),
|
||||||
|
value: text("value").default('{"json": {}}').notNull(), // empty superjson object
|
||||||
|
});
|
||||||
|
|
||||||
export const accountRelations = relations(accounts, ({ one }) => ({
|
export const accountRelations = relations(accounts, ({ one }) => ({
|
||||||
user: one(users, {
|
user: one(users, {
|
||||||
fields: [accounts.userId],
|
fields: [accounts.userId],
|
||||||
|
|||||||
@@ -304,6 +304,11 @@ export const iconRepositories = sqliteTable("iconRepository", {
|
|||||||
slug: text("iconRepository_slug").notNull(),
|
slug: text("iconRepository_slug").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const serverSettings = sqliteTable("serverSetting", {
|
||||||
|
settingKey: text("key").notNull().unique().primaryKey(),
|
||||||
|
value: text("value").default('{"json": {}}').notNull(), // empty superjson object
|
||||||
|
});
|
||||||
|
|
||||||
export const accountRelations = relations(accounts, ({ one }) => ({
|
export const accountRelations = relations(accounts, ({ one }) => ({
|
||||||
user: one(users, {
|
user: one(users, {
|
||||||
fields: [accounts.userId],
|
fields: [accounts.userId],
|
||||||
|
|||||||
1
packages/server-settings/index.ts
Normal file
1
packages/server-settings/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./src";
|
||||||
36
packages/server-settings/package.json
Normal file
36
packages/server-settings/package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "@homarr/server-settings",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rm -rf .turbo node_modules",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"@homarr/eslint-config/base"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": "@homarr/prettier-config"
|
||||||
|
}
|
||||||
16
packages/server-settings/src/index.ts
Normal file
16
packages/server-settings/src/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export const defaultServerSettingsKeys = ["analytics"] as const;
|
||||||
|
|
||||||
|
export type ServerSettingsRecord = {
|
||||||
|
[key in (typeof defaultServerSettingsKeys)[number]]: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultServerSettings = {
|
||||||
|
analytics: {
|
||||||
|
enableGeneral: true,
|
||||||
|
enableWidgetData: false,
|
||||||
|
enableIntegrationData: false,
|
||||||
|
enableUserData: false,
|
||||||
|
},
|
||||||
|
} satisfies ServerSettingsRecord;
|
||||||
|
|
||||||
|
export type ServerSettings = typeof defaultServerSettings;
|
||||||
8
packages/server-settings/tsconfig.json
Normal file
8
packages/server-settings/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@homarr/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
"include": ["*.ts", "src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -1114,6 +1114,7 @@ export default {
|
|||||||
logs: "Logs",
|
logs: "Logs",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
settings: "Settings",
|
||||||
help: {
|
help: {
|
||||||
label: "Help",
|
label: "Help",
|
||||||
items: {
|
items: {
|
||||||
@@ -1283,6 +1284,30 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
title: "Settings",
|
||||||
|
section: {
|
||||||
|
analytics: {
|
||||||
|
title: "Analytics",
|
||||||
|
general: {
|
||||||
|
title: "Send anonymous analytics",
|
||||||
|
text: "Homarr will send anonymized analytics using the open source software Umami. It never collects any personal information and is therefore fully GDPR & CCPA compliant. We encourage you to enable analytics because it helps our open source team to identify issues and prioritize our backlog.",
|
||||||
|
},
|
||||||
|
widgetData: {
|
||||||
|
title: "Widget data",
|
||||||
|
text: "Send which widgets (and their quantity) you have configured. Does not include URLs, names or any other data.",
|
||||||
|
},
|
||||||
|
integrationData: {
|
||||||
|
title: "Integration data",
|
||||||
|
text: "Send which integrations (and their quantity) you have configured. Does not include URLs, names or any other data.",
|
||||||
|
},
|
||||||
|
usersData: {
|
||||||
|
title: "Users data",
|
||||||
|
text: "Send the amount of users and whether you've activated SSO",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
about: {
|
about: {
|
||||||
version: "Version {version}",
|
version: "Version {version}",
|
||||||
text: "Homarr is a community driven open source project that is being maintained by volunteers. Thanks to these people, Homarr has been a growing project since 2021. Our team is working completely remote from many different countries on Homarr in their leisure time for no compensation.",
|
text: "Homarr is a community driven open source project that is being maintained by volunteers. Thanks to these people, Homarr has been a growing project since 2021. Our team is working completely remote from many different countries on Homarr in their leisure time for no compensation.",
|
||||||
|
|||||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -90,6 +90,9 @@ importers:
|
|||||||
'@homarr/notifications':
|
'@homarr/notifications':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../packages/notifications
|
version: link:../../packages/notifications
|
||||||
|
'@homarr/server-settings':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../../packages/server-settings
|
||||||
'@homarr/spotlight':
|
'@homarr/spotlight':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../packages/spotlight
|
version: link:../../packages/spotlight
|
||||||
@@ -247,6 +250,9 @@ importers:
|
|||||||
'@homarr/redis':
|
'@homarr/redis':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../packages/redis
|
version: link:../../packages/redis
|
||||||
|
'@homarr/server-settings':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../../packages/server-settings
|
||||||
'@homarr/validation':
|
'@homarr/validation':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../packages/validation
|
version: link:../../packages/validation
|
||||||
@@ -256,6 +262,9 @@ importers:
|
|||||||
node-cron:
|
node-cron:
|
||||||
specifier: ^3.0.3
|
specifier: ^3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
|
superjson:
|
||||||
|
specifier: 2.2.1
|
||||||
|
version: 2.2.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@homarr/eslint-config':
|
'@homarr/eslint-config':
|
||||||
specifier: workspace:^0.2.0
|
specifier: workspace:^0.2.0
|
||||||
@@ -363,6 +372,9 @@ importers:
|
|||||||
'@homarr/redis':
|
'@homarr/redis':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../redis
|
version: link:../redis
|
||||||
|
'@homarr/server-settings':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../server-settings
|
||||||
'@homarr/tasks':
|
'@homarr/tasks':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../apps/tasks
|
version: link:../../apps/tasks
|
||||||
@@ -713,6 +725,24 @@ importers:
|
|||||||
specifier: ^5.4.5
|
specifier: ^5.4.5
|
||||||
version: 5.4.5
|
version: 5.4.5
|
||||||
|
|
||||||
|
packages/server-settings:
|
||||||
|
devDependencies:
|
||||||
|
'@homarr/eslint-config':
|
||||||
|
specifier: workspace:^0.2.0
|
||||||
|
version: link:../../tooling/eslint
|
||||||
|
'@homarr/prettier-config':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../../tooling/prettier
|
||||||
|
'@homarr/tsconfig':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../../tooling/typescript
|
||||||
|
eslint:
|
||||||
|
specifier: ^8.57.0
|
||||||
|
version: 8.57.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.4.5
|
||||||
|
version: 5.4.5
|
||||||
|
|
||||||
packages/spotlight:
|
packages/spotlight:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@homarr/translation':
|
'@homarr/translation':
|
||||||
|
|||||||
Reference in New Issue
Block a user