chore(release): automatic release v0.1.0
This commit is contained in:
40
.github/renovate.json5
vendored
40
.github/renovate.json5
vendored
@@ -1,31 +1,19 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
extends: ["config:recommended"],
|
||||||
"config:recommended"
|
packageRules: [
|
||||||
],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
{
|
||||||
"matchPackagePatterns": [
|
matchPackagePatterns: ["^@homarr/"],
|
||||||
"^@homarr/",
|
enabled: false,
|
||||||
"tsx" // Disabled for now as version 0.14.4 did not work with the current version of homarr. It resulted in a ERR_MODULE_NOT_FOUND error
|
|
||||||
],
|
|
||||||
"enabled": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": [
|
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
||||||
"minor",
|
automerge: true,
|
||||||
"patch",
|
},
|
||||||
"pin",
|
|
||||||
"digest"
|
|
||||||
],
|
|
||||||
"automerge": true
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"updateInternalDeps": true,
|
updateInternalDeps: true,
|
||||||
"rangeStrategy": "bump",
|
rangeStrategy: "bump",
|
||||||
"automerge": false,
|
automerge: false,
|
||||||
"baseBranches": [
|
baseBranches: ["dev"],
|
||||||
"dev"
|
dependencyDashboard: false,
|
||||||
],
|
}
|
||||||
"dependencyDashboard": false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ COPY --from=builder /app/cli-out/json/ .
|
|||||||
COPY --from=builder /app/next-out/json/ .
|
COPY --from=builder /app/next-out/json/ .
|
||||||
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Is used for postinstall of docs definitions
|
||||||
|
COPY --from=builder /app/packages/definitions/src/docs ./packages/definitions/src/docs
|
||||||
|
|
||||||
# Uses the lockfile to install the dependencies
|
# Uses the lockfile to install the dependencies
|
||||||
RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile
|
RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile
|
||||||
|
|
||||||
|
|||||||
@@ -37,17 +37,17 @@
|
|||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"@mantine/colors-generator": "^7.13.3",
|
"@mantine/colors-generator": "^7.13.4",
|
||||||
"@mantine/core": "^7.13.3",
|
"@mantine/core": "^7.13.4",
|
||||||
"@mantine/hooks": "^7.13.3",
|
"@mantine/hooks": "^7.13.4",
|
||||||
"@mantine/modals": "^7.13.3",
|
"@mantine/modals": "^7.13.4",
|
||||||
"@mantine/tiptap": "^7.13.3",
|
"@mantine/tiptap": "^7.13.4",
|
||||||
"@million/lint": "1.0.11",
|
"@million/lint": "1.0.11",
|
||||||
"@t3-oss/env-nextjs": "^0.11.1",
|
"@t3-oss/env-nextjs": "^0.11.1",
|
||||||
"@tabler/icons-react": "^3.19.0",
|
"@tabler/icons-react": "^3.20.0",
|
||||||
"@tanstack/react-query": "^5.59.15",
|
"@tanstack/react-query": "^5.59.16",
|
||||||
"@tanstack/react-query-devtools": "^5.59.15",
|
"@tanstack/react-query-devtools": "^5.59.16",
|
||||||
"@tanstack/react-query-next-experimental": "5.59.15",
|
"@tanstack/react-query-next-experimental": "5.59.16",
|
||||||
"@trpc/client": "next",
|
"@trpc/client": "next",
|
||||||
"@trpc/next": "next",
|
"@trpc/next": "next",
|
||||||
"@trpc/react-query": "next",
|
"@trpc/react-query": "next",
|
||||||
@@ -63,14 +63,14 @@
|
|||||||
"glob": "^11.0.0",
|
"glob": "^11.0.0",
|
||||||
"jotai": "^2.10.1",
|
"jotai": "^2.10.1",
|
||||||
"mantine-react-table": "2.0.0-beta.7",
|
"mantine-react-table": "2.0.0-beta.7",
|
||||||
"next": "^14.2.15",
|
"next": "^14.2.16",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-error-boundary": "^4.1.1",
|
"react-error-boundary": "^4.1.2",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "^1.80.2",
|
"sass": "^1.80.4",
|
||||||
"superjson": "2.2.1",
|
"superjson": "2.2.1",
|
||||||
"swagger-ui-react": "^5.17.14",
|
"swagger-ui-react": "^5.17.14",
|
||||||
"use-deep-compare-effect": "^1.8.1"
|
"use-deep-compare-effect": "^1.8.1"
|
||||||
@@ -80,13 +80,13 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/chroma-js": "2.4.4",
|
"@types/chroma-js": "2.4.4",
|
||||||
"@types/node": "^20.16.12",
|
"@types/node": "^20.17.1",
|
||||||
"@types/prismjs": "^1.26.4",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "^18.3.11",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"node-loader": "^2.0.0",
|
"node-loader": "^2.0.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import { useState } from "react";
|
|
||||||
import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core";
|
import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core";
|
||||||
import { createTheme, DirectionProvider, isMantineColorScheme, MantineProvider } from "@mantine/core";
|
import { createTheme, DirectionProvider, MantineProvider } from "@mantine/core";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -32,27 +31,23 @@ export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
|
|||||||
function useColorSchemeManager(): MantineColorSchemeManager {
|
function useColorSchemeManager(): MantineColorSchemeManager {
|
||||||
const key = "homarr-color-scheme";
|
const key = "homarr-color-scheme";
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const [sessionColorScheme, setSessionColorScheme] = useState<MantineColorScheme | undefined>(
|
|
||||||
session?.user.colorScheme,
|
const updateCookieValue = (value: Exclude<MantineColorScheme, "auto">) => {
|
||||||
);
|
setClientCookie(key, value, { expires: dayjs().add(1, "year").toDate(), path: "/" });
|
||||||
|
};
|
||||||
|
|
||||||
const { mutate: mutateColorScheme } = clientApi.user.changeColorScheme.useMutation({
|
const { mutate: mutateColorScheme } = clientApi.user.changeColorScheme.useMutation({
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
setSessionColorScheme(variables.colorScheme);
|
updateCookieValue(variables.colorScheme);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let handleStorageEvent: (event: StorageEvent) => void;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get: (defaultValue) => {
|
get: (defaultValue) => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionColorScheme) {
|
|
||||||
return sessionColorScheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cookies = parseCookies(document.cookie);
|
const cookies = parseCookies(document.cookie);
|
||||||
return (cookies[key] as MantineColorScheme | undefined) ?? defaultValue;
|
return (cookies[key] as MantineColorScheme | undefined) ?? defaultValue;
|
||||||
@@ -67,30 +62,13 @@ function useColorSchemeManager(): MantineColorSchemeManager {
|
|||||||
if (session) {
|
if (session) {
|
||||||
mutateColorScheme({ colorScheme: value });
|
mutateColorScheme({ colorScheme: value });
|
||||||
}
|
}
|
||||||
setClientCookie(key, value, { expires: dayjs().add(1, "year").toDate() });
|
updateCookieValue(value);
|
||||||
window.localStorage.setItem(key, value);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[@mantine/core] Local storage color scheme manager was unable to save color scheme.", error);
|
console.warn("[@mantine/core] Color scheme manager was unable to save color scheme.", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
subscribe: () => undefined,
|
||||||
subscribe: (onUpdate) => {
|
unsubscribe: () => undefined,
|
||||||
handleStorageEvent = (event) => {
|
clear: () => undefined,
|
||||||
if (session) return; // Ignore updates when session is available as we are using session color scheme
|
|
||||||
if (event.storageArea === window.localStorage && event.key === key && isMantineColorScheme(event.newValue)) {
|
|
||||||
onUpdate(event.newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("storage", handleStorageEvent);
|
|
||||||
},
|
|
||||||
|
|
||||||
unsubscribe: () => {
|
|
||||||
window.removeEventListener("storage", handleStorageEvent);
|
|
||||||
},
|
|
||||||
|
|
||||||
clear: () => {
|
|
||||||
window.localStorage.removeItem(key);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,23 @@ import superjson from "superjson";
|
|||||||
import type { AppRouter } from "@homarr/api";
|
import type { AppRouter } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
|
const constructWebsocketUrl = () => {
|
||||||
|
const fallback = "ws://localhost:3001/websockets";
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.NODE_ENV === "development") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `ws://${window.location.hostname}:${window.location.port}/websockets`;
|
||||||
|
};
|
||||||
|
|
||||||
const wsClient = createWSClient({
|
const wsClient = createWSClient({
|
||||||
url:
|
url: constructWebsocketUrl(),
|
||||||
typeof window === "undefined"
|
|
||||||
? "ws://localhost:3001/websockets"
|
|
||||||
: `ws://${window.location.hostname}:${window.location.port}/websockets`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function TRPCReactProvider(props: PropsWithChildren) {
|
export function TRPCReactProvider(props: PropsWithChildren) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
|
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { isProviderEnabled } from "@homarr/auth/server";
|
import { isProviderEnabled } from "@homarr/auth/server";
|
||||||
|
import { createDocumentationLink } from "@homarr/definitions";
|
||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
import { MainHeader } from "~/components/layout/header";
|
import { MainHeader } from "~/components/layout/header";
|
||||||
@@ -124,7 +125,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
|||||||
{
|
{
|
||||||
label: t("items.help.items.documentation"),
|
label: t("items.help.items.documentation"),
|
||||||
icon: IconBook2,
|
icon: IconBook2,
|
||||||
href: "https://homarr.dev/docs/getting-started/",
|
href: createDocumentationLink("/docs/getting-started"),
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ interface RenameGroupFormProps {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
|
export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
|
const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
|
||||||
const form = useZodForm(validation.group.update.pick({ name: true }), {
|
const form = useZodForm(validation.group.update.pick({ name: true }), {
|
||||||
@@ -28,6 +29,9 @@ export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
|
|||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(values: FormType) => {
|
(values: FormType) => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mutate(
|
mutate(
|
||||||
{
|
{
|
||||||
...values,
|
...values,
|
||||||
@@ -60,13 +64,15 @@ export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextInput label={t("group.field.name")} {...form.getInputProps("name")} />
|
<TextInput label={t("group.field.name")} {...form.getInputProps("name")} disabled={disabled} />
|
||||||
|
|
||||||
<Group justify="end">
|
{!disabled && (
|
||||||
<Button type="submit" color="teal" loading={isPending}>
|
<Group justify="end">
|
||||||
{t("common.action.saveChanges")}
|
<Button type="submit" color="teal" loading={isPending}>
|
||||||
</Button>
|
{t("common.action.saveChanges")}
|
||||||
</Group>
|
</Button>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Alert, Anchor } from "@mantine/core";
|
||||||
|
import { IconExclamationCircle } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { createDocumentationLink } from "@homarr/definitions";
|
||||||
|
import { getI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
|
export const ReservedGroupAlert = async () => {
|
||||||
|
const t = await getI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert variant="light" color="yellow" icon={<IconExclamationCircle size="1rem" stroke={1.5} />}>
|
||||||
|
{t("group.reservedNotice.message", {
|
||||||
|
checkoutDocs: (
|
||||||
|
<Anchor
|
||||||
|
size="sm"
|
||||||
|
component={Link}
|
||||||
|
href={createDocumentationLink("/docs/management/users", "#special-groups")}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{t("common.action.checkoutDocs")}
|
||||||
|
</Anchor>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,9 +6,11 @@ import type { RouterOutputs } from "@homarr/api";
|
|||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
import { env } from "@homarr/auth/env.mjs";
|
import { env } from "@homarr/auth/env.mjs";
|
||||||
import { isProviderEnabled } from "@homarr/auth/server";
|
import { isProviderEnabled } from "@homarr/auth/server";
|
||||||
|
import { everyoneGroup } from "@homarr/definitions";
|
||||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||||
import { SearchInput, UserAvatar } from "@homarr/ui";
|
import { SearchInput, UserAvatar } from "@homarr/ui";
|
||||||
|
|
||||||
|
import { ReservedGroupAlert } from "../_reserved-group-alert";
|
||||||
import { AddGroupMember } from "./_add-group-member";
|
import { AddGroupMember } from "./_add-group-member";
|
||||||
import { RemoveGroupMember } from "./_remove-group-member";
|
import { RemoveGroupMember } from "./_remove-group-member";
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
|
|||||||
const t = await getI18n();
|
const t = await getI18n();
|
||||||
const tMembers = await getScopedI18n("management.page.group.setting.members");
|
const tMembers = await getScopedI18n("management.page.group.setting.members");
|
||||||
const group = await api.group.getById({ id: params.id });
|
const group = await api.group.getById({ id: params.id });
|
||||||
|
const isReserved = group.name === everyoneGroup;
|
||||||
|
|
||||||
const filteredMembers = searchParams.search
|
const filteredMembers = searchParams.search
|
||||||
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
@@ -41,15 +44,19 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Title>{tMembers("title")}</Title>
|
<Title>{tMembers("title")}</Title>
|
||||||
|
|
||||||
{providerTypes !== "credentials" && (
|
{isReserved ? (
|
||||||
<Alert variant="light" color="yellow" icon={<IconExclamationCircle size="1rem" stroke={1.5} />}>
|
<ReservedGroupAlert />
|
||||||
{t(`group.memberNotice.${providerTypes}`)}
|
) : (
|
||||||
</Alert>
|
providerTypes !== "credentials" && (
|
||||||
|
<Alert variant="light" color="yellow" icon={<IconExclamationCircle size="1rem" stroke={1.5} />}>
|
||||||
|
{t(`group.memberNotice.${providerTypes}`)}
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<SearchInput placeholder={`${tMembers("search")}...`} defaultValue={searchParams.search} />
|
<SearchInput placeholder={`${tMembers("search")}...`} defaultValue={searchParams.search} />
|
||||||
{isProviderEnabled("credentials") && (
|
{isProviderEnabled("credentials") && !isReserved && (
|
||||||
<AddGroupMember groupId={group.id} presentUserIds={group.members.map((member) => member.id)} />
|
<AddGroupMember groupId={group.id} presentUserIds={group.members.map((member) => member.id)} />
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
@@ -63,7 +70,7 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
|
|||||||
<Table striped highlightOnHover>
|
<Table striped highlightOnHover>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredMembers.map((member) => (
|
{filteredMembers.map((member) => (
|
||||||
<Row key={group.id} member={member} groupId={group.id} />
|
<Row key={group.id} member={member} groupId={group.id} disabled={isReserved} />
|
||||||
))}
|
))}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
@@ -74,9 +81,10 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
|
|||||||
interface RowProps {
|
interface RowProps {
|
||||||
member: RouterOutputs["group"]["getById"]["members"][number];
|
member: RouterOutputs["group"]["getById"]["members"][number];
|
||||||
groupId: string;
|
groupId: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row = ({ member, groupId }: RowProps) => {
|
const Row = ({ member, groupId, disabled }: RowProps) => {
|
||||||
return (
|
return (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
@@ -88,7 +96,7 @@ const Row = ({ member, groupId }: RowProps) => {
|
|||||||
</Group>
|
</Group>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd w={100}>
|
<TableTd w={100}>
|
||||||
{member.provider === "credentials" && <RemoveGroupMember user={member} groupId={groupId} />}
|
{member.provider === "credentials" && !disabled && <RemoveGroupMember user={member} groupId={groupId} />}
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Stack, Title } from "@mantine/core";
|
import { Stack, Title } from "@mantine/core";
|
||||||
|
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
|
import { everyoneGroup } from "@homarr/definitions";
|
||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
|
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
|
||||||
import { DeleteGroup } from "./_delete-group";
|
import { DeleteGroup } from "./_delete-group";
|
||||||
import { RenameGroupForm } from "./_rename-group-form";
|
import { RenameGroupForm } from "./_rename-group-form";
|
||||||
|
import { ReservedGroupAlert } from "./_reserved-group-alert";
|
||||||
import { TransferGroupOwnership } from "./_transfer-group-ownership";
|
import { TransferGroupOwnership } from "./_transfer-group-ownership";
|
||||||
|
|
||||||
interface GroupsDetailPageProps {
|
interface GroupsDetailPageProps {
|
||||||
@@ -18,26 +20,31 @@ export default async function GroupsDetailPage({ params }: GroupsDetailPageProps
|
|||||||
const group = await api.group.getById({ id: params.id });
|
const group = await api.group.getById({ id: params.id });
|
||||||
const tGeneral = await getScopedI18n("management.page.group.setting.general");
|
const tGeneral = await getScopedI18n("management.page.group.setting.general");
|
||||||
const tGroupAction = await getScopedI18n("group.action");
|
const tGroupAction = await getScopedI18n("group.action");
|
||||||
|
const isReserved = group.name === everyoneGroup;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Title>{tGeneral("title")}</Title>
|
<Title>{tGeneral("title")}</Title>
|
||||||
|
|
||||||
<RenameGroupForm group={group} />
|
{isReserved && <ReservedGroupAlert />}
|
||||||
|
|
||||||
<DangerZoneRoot>
|
<RenameGroupForm group={group} disabled={isReserved} />
|
||||||
<DangerZoneItem
|
|
||||||
label={tGroupAction("transfer.label")}
|
|
||||||
description={tGroupAction("transfer.description")}
|
|
||||||
action={<TransferGroupOwnership group={group} />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DangerZoneItem
|
{!isReserved && (
|
||||||
label={tGroupAction("delete.label")}
|
<DangerZoneRoot>
|
||||||
description={tGroupAction("delete.description")}
|
<DangerZoneItem
|
||||||
action={<DeleteGroup group={group} />}
|
label={tGroupAction("transfer.label")}
|
||||||
/>
|
description={tGroupAction("transfer.description")}
|
||||||
</DangerZoneRoot>
|
action={<TransferGroupOwnership group={group} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DangerZoneItem
|
||||||
|
label={tGroupAction("delete.label")}
|
||||||
|
description={tGroupAction("delete.description")}
|
||||||
|
action={<DeleteGroup group={group} />}
|
||||||
|
/>
|
||||||
|
</DangerZoneRoot>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,11 +44,11 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node": "^20.16.12",
|
"@types/node": "^20.17.1",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"tsx": "4.13.3",
|
"tsx": "4.19.1",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import "./undici-log-agent-override";
|
|||||||
import { registerCronJobRunner } from "@homarr/cron-job-runner";
|
import { registerCronJobRunner } from "@homarr/cron-job-runner";
|
||||||
import { jobGroup } from "@homarr/cron-jobs";
|
import { jobGroup } from "@homarr/cron-jobs";
|
||||||
|
|
||||||
import { seedServerSettingsAsync } from "./seed-server-settings";
|
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
registerCronJobRunner();
|
registerCronJobRunner();
|
||||||
await jobGroup.startAllAsync();
|
await jobGroup.startAllAsync();
|
||||||
await seedServerSettingsAsync();
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"tsx": "4.13.3",
|
"tsx": "4.19.1",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/ws": "^8.5.12",
|
"@types/ws": "^8.5.12",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
"clean": "git clean -xdf node_modules",
|
"clean": "git clean -xdf node_modules",
|
||||||
"clean:workspaces": "turbo clean",
|
"clean:workspaces": "turbo clean",
|
||||||
"cli": "pnpm with-env tsx packages/cli/index.ts",
|
"cli": "pnpm with-env tsx packages/cli/index.ts",
|
||||||
@@ -27,15 +27,15 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@turbo/gen": "^2.1.3",
|
"@turbo/gen": "^2.2.3",
|
||||||
"@vitejs/plugin-react": "^4.3.2",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
"@vitest/coverage-v8": "^2.1.3",
|
"@vitest/coverage-v8": "^2.1.3",
|
||||||
"@vitest/ui": "^2.1.3",
|
"@vitest/ui": "^2.1.3",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"testcontainers": "^10.13.2",
|
"testcontainers": "^10.13.2",
|
||||||
"turbo": "^2.1.3",
|
"turbo": "^2.2.3",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite-tsconfig-paths": "^5.0.1",
|
"vite-tsconfig-paths": "^5.0.1",
|
||||||
"vitest": "^2.1.3"
|
"vitest": "^2.1.3"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"@trpc/react-query": "next",
|
"@trpc/react-query": "next",
|
||||||
"@trpc/server": "next",
|
"@trpc/server": "next",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"next": "^14.2.15",
|
"next": "^14.2.16",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"superjson": "2.2.1",
|
"superjson": "2.2.1",
|
||||||
"trpc-swagger": "^1.2.6"
|
"trpc-swagger": "^1.2.6"
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/dockerode": "^3.3.31",
|
"@types/dockerode": "^3.3.31",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { and, createId, eq, like, not, sql } from "@homarr/db";
|
import { and, createId, eq, like, not, sql } from "@homarr/db";
|
||||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema/sqlite";
|
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema/sqlite";
|
||||||
|
import { everyoneGroup } from "@homarr/definitions";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation, z } from "@homarr/validation";
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../trpc";
|
||||||
@@ -121,13 +122,12 @@ export const groupRouter = createTRPCRouter({
|
|||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(validation.group.create)
|
.input(validation.group.create)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const normalizedName = normalizeName(input.name);
|
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
||||||
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName);
|
|
||||||
|
|
||||||
const id = createId();
|
const id = createId();
|
||||||
await ctx.db.insert(groups).values({
|
await ctx.db.insert(groups).values({
|
||||||
id,
|
id,
|
||||||
name: normalizedName,
|
name: input.name,
|
||||||
ownerId: ctx.session.user.id,
|
ownerId: ctx.session.user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,14 +138,14 @@ export const groupRouter = createTRPCRouter({
|
|||||||
.input(validation.group.update)
|
.input(validation.group.update)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||||
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
||||||
|
|
||||||
const normalizedName = normalizeName(input.name);
|
await checkSimilarNameAndThrowAsync(ctx.db, input.name, input.id);
|
||||||
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName, input.id);
|
|
||||||
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(groups)
|
.update(groups)
|
||||||
.set({
|
.set({
|
||||||
name: normalizedName,
|
name: input.name,
|
||||||
})
|
})
|
||||||
.where(eq(groups.id, input.id));
|
.where(eq(groups.id, input.id));
|
||||||
}),
|
}),
|
||||||
@@ -169,6 +169,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
.input(validation.group.groupUser)
|
.input(validation.group.groupUser)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||||
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||||
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(groups)
|
.update(groups)
|
||||||
@@ -182,6 +183,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
.input(validation.common.byId)
|
.input(validation.common.byId)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||||
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
||||||
|
|
||||||
await ctx.db.delete(groups).where(eq(groups.id, input.id));
|
await ctx.db.delete(groups).where(eq(groups.id, input.id));
|
||||||
}),
|
}),
|
||||||
@@ -190,6 +192,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
.input(validation.group.groupUser)
|
.input(validation.group.groupUser)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||||
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||||
throwIfCredentialsDisabled();
|
throwIfCredentialsDisabled();
|
||||||
|
|
||||||
const user = await ctx.db.query.users.findFirst({
|
const user = await ctx.db.query.users.findFirst({
|
||||||
@@ -213,6 +216,7 @@ export const groupRouter = createTRPCRouter({
|
|||||||
.input(validation.group.groupUser)
|
.input(validation.group.groupUser)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||||
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||||
throwIfCredentialsDisabled();
|
throwIfCredentialsDisabled();
|
||||||
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
@@ -221,8 +225,6 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeName = (name: string) => name.trim();
|
|
||||||
|
|
||||||
const checkSimilarNameAndThrowAsync = async (db: Database, name: string, ignoreId?: string) => {
|
const checkSimilarNameAndThrowAsync = async (db: Database, name: string, ignoreId?: string) => {
|
||||||
const similar = await db.query.groups.findFirst({
|
const similar = await db.query.groups.findFirst({
|
||||||
where: and(like(groups.name, `${name}`), not(eq(groups.id, ignoreId ?? ""))),
|
where: and(like(groups.name, `${name}`), not(eq(groups.id, ignoreId ?? ""))),
|
||||||
@@ -236,6 +238,17 @@ const checkSimilarNameAndThrowAsync = async (db: Database, name: string, ignoreI
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const throwIfGroupNameIsReservedAsync = async (db: Database, id: string) => {
|
||||||
|
const count = await db.$count(groups, and(eq(groups.id, id), eq(groups.name, everyoneGroup)));
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Action is forbidden for reserved group names",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const throwIfGroupNotFoundAsync = async (db: Database, id: string) => {
|
const throwIfGroupNotFoundAsync = async (db: Database, id: string) => {
|
||||||
const group = await db.query.groups.findFirst({
|
const group = await db.query.groups.findFirst({
|
||||||
where: eq(groups.id, id),
|
where: eq(groups.id, id),
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const healthMonitoringRouter = createTRPCRouter({
|
|||||||
emit.next({
|
emit.next({
|
||||||
integrationId: integration.id,
|
integrationId: integration.id,
|
||||||
healthInfo,
|
healthInfo,
|
||||||
timestamp: new Date(0),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
unsubscribes.push(unsubscribe);
|
unsubscribes.push(unsubscribe);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { NextAuthConfig } from "next-auth";
|
|||||||
import { and, eq, inArray } from "@homarr/db";
|
import { and, eq, inArray } from "@homarr/db";
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { groupMembers, groups, users } from "@homarr/db/schema/sqlite";
|
import { groupMembers, groups, users } from "@homarr/db/schema/sqlite";
|
||||||
|
import { everyoneGroup } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
import { env } from "./env.mjs";
|
import { env } from "./env.mjs";
|
||||||
@@ -33,6 +34,7 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
|||||||
if ("groups" in user && Array.isArray(user.groups)) {
|
if ("groups" in user && Array.isArray(user.groups)) {
|
||||||
await synchronizeGroupsWithExternalForUserAsync(db, user.id, user.groups as string[]);
|
await synchronizeGroupsWithExternalForUserAsync(db, user.id, user.groups as string[]);
|
||||||
}
|
}
|
||||||
|
await addUserToEveryoneGroupIfNotMemberAsync(db, user.id);
|
||||||
|
|
||||||
if (dbUser.name !== user.name) {
|
if (dbUser.name !== user.name) {
|
||||||
await db.update(users).set({ name: user.name }).where(eq(users.id, user.id));
|
await db.update(users).set({ name: user.name }).where(eq(users.id, user.id));
|
||||||
@@ -57,7 +59,27 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addUserToEveryoneGroupIfNotMemberAsync = async (db: Database, userId: string) => {
|
||||||
|
const dbEveryoneGroup = await db.query.groups.findFirst({
|
||||||
|
where: eq(groups.name, everyoneGroup),
|
||||||
|
with: {
|
||||||
|
members: {
|
||||||
|
where: eq(groupMembers.userId, userId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dbEveryoneGroup?.members.length === 0) {
|
||||||
|
await db.insert(groupMembers).values({
|
||||||
|
userId,
|
||||||
|
groupId: dbEveryoneGroup.id,
|
||||||
|
});
|
||||||
|
logger.info(`Added user to everyone group. user=${userId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: string, externalGroups: string[]) => {
|
const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: string, externalGroups: string[]) => {
|
||||||
|
const ignoredGroups = [everyoneGroup];
|
||||||
const dbGroupMembers = await db.query.groupMembers.findMany({
|
const dbGroupMembers = await db.query.groupMembers.findMany({
|
||||||
where: eq(groupMembers.userId, userId),
|
where: eq(groupMembers.userId, userId),
|
||||||
with: {
|
with: {
|
||||||
@@ -102,11 +124,11 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The below groups are those groups the user is part of in Homarr, but not in the external system.
|
* The below groups are those groups the user is part of in Homarr, but not in the external system and not ignored.
|
||||||
* So he has to be removed from those groups.
|
* So he has to be removed from those groups.
|
||||||
*/
|
*/
|
||||||
const groupsUserIsNoLongerMemberOfExternally = dbGroupMembers.filter(
|
const groupsUserIsNoLongerMemberOfExternally = dbGroupMembers.filter(
|
||||||
({ group }) => !externalGroups.includes(group.name),
|
({ group }) => !externalGroups.concat(ignoredGroups).includes(group.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (groupsUserIsNoLongerMemberOfExternally.length > 0) {
|
if (groupsUserIsNoLongerMemberOfExternally.length > 0) {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.37.1",
|
"@auth/core": "^0.37.2",
|
||||||
"@auth/drizzle-adapter": "^1.7.1",
|
"@auth/drizzle-adapter": "^1.7.2",
|
||||||
"@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",
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"ldapts": "7.2.1",
|
"ldapts": "7.2.1",
|
||||||
"next": "^14.2.15",
|
"next": "^14.2.16",
|
||||||
"next-auth": "5.0.0-beta.23",
|
"next-auth": "5.0.0-beta.25",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/cookies": "0.9.0",
|
"@types/cookies": "0.9.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { eq } from "@homarr/db";
|
|||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { groupMembers, groups, users } from "@homarr/db/schema/sqlite";
|
import { groupMembers, groups, users } from "@homarr/db/schema/sqlite";
|
||||||
import { createDb } from "@homarr/db/test";
|
import { createDb } from "@homarr/db/test";
|
||||||
|
import { everyoneGroup } from "@homarr/definitions";
|
||||||
|
|
||||||
import { createSignInEventHandler } from "../events";
|
import { createSignInEventHandler } from "../events";
|
||||||
|
|
||||||
@@ -34,6 +35,29 @@ vi.mock("next/headers", async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("createSignInEventHandler should create signInEventHandler", () => {
|
describe("createSignInEventHandler should create signInEventHandler", () => {
|
||||||
|
describe("signInEventHandler should add users to everyone group", () => {
|
||||||
|
test("should add user to everyone group if he isn't already", async () => {
|
||||||
|
// Arrange
|
||||||
|
const db = createDb();
|
||||||
|
await createUserAsync(db);
|
||||||
|
await createGroupAsync(db, everyoneGroup);
|
||||||
|
const eventHandler = createSignInEventHandler(db);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await eventHandler?.({
|
||||||
|
user: { id: "1", name: "test" },
|
||||||
|
profile: undefined,
|
||||||
|
account: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const dbGroupMembers = await db.query.groupMembers.findFirst({
|
||||||
|
where: eq(groupMembers.userId, "1"),
|
||||||
|
});
|
||||||
|
expect(dbGroupMembers?.groupId).toBe("1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("signInEventHandler should synchronize ldap groups", () => {
|
describe("signInEventHandler should synchronize ldap groups", () => {
|
||||||
test("should add missing group membership", async () => {
|
test("should add missing group membership", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -79,6 +103,30 @@ describe("createSignInEventHandler should create signInEventHandler", () => {
|
|||||||
});
|
});
|
||||||
expect(dbGroupMembers).toBeUndefined();
|
expect(dbGroupMembers).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
test("should not remove group membership for everyone group", async () => {
|
||||||
|
// Arrange
|
||||||
|
const db = createDb();
|
||||||
|
await createUserAsync(db);
|
||||||
|
await createGroupAsync(db, everyoneGroup);
|
||||||
|
await db.insert(groupMembers).values({
|
||||||
|
userId: "1",
|
||||||
|
groupId: "1",
|
||||||
|
});
|
||||||
|
const eventHandler = createSignInEventHandler(db);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await eventHandler?.({
|
||||||
|
user: { id: "1", name: "test", groups: [] } as never,
|
||||||
|
profile: undefined,
|
||||||
|
account: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const dbGroupMembers = await db.query.groupMembers.findFirst({
|
||||||
|
where: eq(groupMembers.userId, "1"),
|
||||||
|
});
|
||||||
|
expect(dbGroupMembers?.groupId).toBe("1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe("signInEventHandler should synchronize oidc groups", () => {
|
describe("signInEventHandler should synchronize oidc groups", () => {
|
||||||
test("should add missing group membership", async () => {
|
test("should add missing group membership", async () => {
|
||||||
@@ -125,6 +173,30 @@ describe("createSignInEventHandler should create signInEventHandler", () => {
|
|||||||
});
|
});
|
||||||
expect(dbGroupMembers).toBeUndefined();
|
expect(dbGroupMembers).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
test("should not remove group membership for everyone group", async () => {
|
||||||
|
// Arrange
|
||||||
|
const db = createDb();
|
||||||
|
await createUserAsync(db);
|
||||||
|
await createGroupAsync(db, everyoneGroup);
|
||||||
|
await db.insert(groupMembers).values({
|
||||||
|
userId: "1",
|
||||||
|
groupId: "1",
|
||||||
|
});
|
||||||
|
const eventHandler = createSignInEventHandler(db);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await eventHandler?.({
|
||||||
|
user: { id: "1", name: "test" },
|
||||||
|
profile: { preferred_username: "test", someRandomGroupsKey: [] },
|
||||||
|
account: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const dbGroupMembers = await db.query.groupMembers.findFirst({
|
||||||
|
where: eq(groupMembers.userId, "1"),
|
||||||
|
});
|
||||||
|
expect(dbGroupMembers?.groupId).toBe("1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
test.each([
|
test.each([
|
||||||
["ldap" as const, { name: "test-new" }, undefined],
|
["ldap" as const, { name: "test-new" }, undefined],
|
||||||
@@ -183,8 +255,8 @@ const createUserAsync = async (db: Database) =>
|
|||||||
colorScheme: "dark",
|
colorScheme: "dark",
|
||||||
});
|
});
|
||||||
|
|
||||||
const createGroupAsync = async (db: Database) =>
|
const createGroupAsync = async (db: Database, name = "test") =>
|
||||||
await db.insert(groups).values({
|
await db.insert(groups).values({
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "test",
|
name,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"next": "^14.2.15",
|
"next": "^14.2.16",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"tldts": "^6.1.52"
|
"tldts": "^6.1.55"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `user` MODIFY COLUMN `colorScheme` varchar(5) NOT NULL DEFAULT 'dark';
|
||||||
1
packages/db/migrations/mysql/0013_youthful_vulture.sql
Normal file
1
packages/db/migrations/mysql/0013_youthful_vulture.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `group` ADD CONSTRAINT `group_name_unique` UNIQUE(`name`);
|
||||||
1522
packages/db/migrations/mysql/meta/0012_snapshot.json
Normal file
1522
packages/db/migrations/mysql/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1527
packages/db/migrations/mysql/meta/0013_snapshot.json
Normal file
1527
packages/db/migrations/mysql/meta/0013_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,20 @@
|
|||||||
"when": 1728490046896,
|
"when": 1728490046896,
|
||||||
"tag": "0011_freezing_banshee",
|
"tag": "0011_freezing_banshee",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1729348221072,
|
||||||
|
"tag": "0012_abnormal_wendell_vaughn",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 13,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1729369383739,
|
||||||
|
"tag": "0013_youthful_vulture",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,35 @@ import { drizzle } from "drizzle-orm/mysql2";
|
|||||||
import { migrate } from "drizzle-orm/mysql2/migrator";
|
import { migrate } from "drizzle-orm/mysql2/migrator";
|
||||||
import mysql from "mysql2";
|
import mysql from "mysql2";
|
||||||
|
|
||||||
|
import type { Database } from "../..";
|
||||||
|
import * as mysqlSchema from "../../schema/mysql";
|
||||||
|
import { seedDataAsync } from "../seed";
|
||||||
|
|
||||||
const migrationsFolder = process.argv[2] ?? ".";
|
const migrationsFolder = process.argv[2] ?? ".";
|
||||||
|
|
||||||
const mysql2 = mysql.createConnection(
|
const migrateAsync = async () => {
|
||||||
process.env.DB_HOST
|
const mysql2 = mysql.createConnection(
|
||||||
? {
|
process.env.DB_HOST
|
||||||
host: process.env.DB_HOST,
|
? {
|
||||||
database: process.env.DB_NAME!,
|
host: process.env.DB_HOST,
|
||||||
port: Number(process.env.DB_PORT),
|
database: process.env.DB_NAME!,
|
||||||
user: process.env.DB_USER,
|
port: Number(process.env.DB_PORT),
|
||||||
password: process.env.DB_PASSWORD,
|
user: process.env.DB_USER,
|
||||||
}
|
password: process.env.DB_PASSWORD,
|
||||||
: { uri: process.env.DB_URL },
|
}
|
||||||
);
|
: { uri: process.env.DB_URL },
|
||||||
|
);
|
||||||
|
|
||||||
const db = drizzle(mysql2, {
|
const db = drizzle(mysql2, {
|
||||||
mode: "default",
|
mode: "default",
|
||||||
});
|
schema: mysqlSchema,
|
||||||
|
});
|
||||||
|
|
||||||
migrate(db, { migrationsFolder })
|
await migrate(db, { migrationsFolder });
|
||||||
|
await seedDataAsync(db as unknown as Database);
|
||||||
|
};
|
||||||
|
|
||||||
|
migrateAsync()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("Migration complete");
|
console.log("Migration complete");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
12
packages/db/migrations/run-seed.ts
Normal file
12
packages/db/migrations/run-seed.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { database } from "../driver";
|
||||||
|
import { seedDataAsync } from "./seed";
|
||||||
|
|
||||||
|
seedDataAsync(database)
|
||||||
|
.then(() => {
|
||||||
|
console.log("Seed complete");
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("Seed failed\n\t", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
52
packages/db/migrations/seed.ts
Normal file
52
packages/db/migrations/seed.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
|
import { everyoneGroup } from "@homarr/definitions";
|
||||||
|
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||||
|
|
||||||
|
import { createId, eq } from "..";
|
||||||
|
import type { Database } from "..";
|
||||||
|
import { groups } from "../schema/mysql";
|
||||||
|
import { serverSettings } from "../schema/sqlite";
|
||||||
|
|
||||||
|
export const seedDataAsync = async (db: Database) => {
|
||||||
|
await seedEveryoneGroupAsync(db);
|
||||||
|
await seedServerSettingsAsync(db);
|
||||||
|
};
|
||||||
|
|
||||||
|
const seedEveryoneGroupAsync = async (db: Database) => {
|
||||||
|
const group = await db.query.groups.findFirst({
|
||||||
|
where: eq(groups.name, everyoneGroup),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
console.log("Skipping seeding of group 'everyone' as it already exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(groups).values({
|
||||||
|
id: createId(),
|
||||||
|
name: everyoneGroup,
|
||||||
|
});
|
||||||
|
console.log("Created group 'everyone' through seed");
|
||||||
|
};
|
||||||
|
|
||||||
|
const seedServerSettingsAsync = async (db: Database) => {
|
||||||
|
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) {
|
||||||
|
console.info(`Inserted ${insertedSettingsCount} missing settings`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_user` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text,
|
||||||
|
`email` text,
|
||||||
|
`emailVerified` integer,
|
||||||
|
`image` text,
|
||||||
|
`password` text,
|
||||||
|
`salt` text,
|
||||||
|
`provider` text DEFAULT 'credentials' NOT NULL,
|
||||||
|
`homeBoardId` text,
|
||||||
|
`colorScheme` text DEFAULT 'dark' NOT NULL,
|
||||||
|
`firstDayOfWeek` integer DEFAULT 1 NOT NULL,
|
||||||
|
`pingIconsEnabled` integer DEFAULT false NOT NULL,
|
||||||
|
FOREIGN KEY (`homeBoardId`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE set null
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_user`("id", "name", "email", "emailVerified", "image", "password", "salt", "provider", "homeBoardId", "colorScheme", "firstDayOfWeek", "pingIconsEnabled") SELECT "id", "name", "email", "emailVerified", "image", "password", "salt", "provider", "homeBoardId", "colorScheme", "firstDayOfWeek", "pingIconsEnabled" FROM `user`;--> statement-breakpoint
|
||||||
|
DROP TABLE `user`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_user` RENAME TO `user`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
1
packages/db/migrations/sqlite/0013_faithful_hex.sql
Normal file
1
packages/db/migrations/sqlite/0013_faithful_hex.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE UNIQUE INDEX `group_name_unique` ON `group` (`name`);
|
||||||
1455
packages/db/migrations/sqlite/meta/0012_snapshot.json
Normal file
1455
packages/db/migrations/sqlite/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1461
packages/db/migrations/sqlite/meta/0013_snapshot.json
Normal file
1461
packages/db/migrations/sqlite/meta/0013_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,20 @@
|
|||||||
"when": 1728490026154,
|
"when": 1728490026154,
|
||||||
"tag": "0011_classy_angel",
|
"tag": "0011_classy_angel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1729348200091,
|
||||||
|
"tag": "0012_ambiguous_black_panther",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 13,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1729369389386,
|
||||||
|
"tag": "0013_faithful_hex",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,27 @@ import Database from "better-sqlite3";
|
|||||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
|
|
||||||
|
import { schema } from "../..";
|
||||||
|
import { seedDataAsync } from "../seed";
|
||||||
|
|
||||||
const migrationsFolder = process.argv[2] ?? ".";
|
const migrationsFolder = process.argv[2] ?? ".";
|
||||||
|
|
||||||
const sqlite = new Database(process.env.DB_URL?.replace("file:", ""));
|
const migrateAsync = async () => {
|
||||||
|
const sqlite = new Database(process.env.DB_URL?.replace("file:", ""));
|
||||||
|
|
||||||
const db = drizzle(sqlite);
|
const db = drizzle(sqlite, { schema });
|
||||||
|
|
||||||
migrate(db, { migrationsFolder });
|
migrate(db, { migrationsFolder });
|
||||||
|
|
||||||
|
await seedDataAsync(db);
|
||||||
|
};
|
||||||
|
|
||||||
|
migrateAsync()
|
||||||
|
.then(() => {
|
||||||
|
console.log("Migration complete");
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("Migration failed", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|||||||
@@ -21,28 +21,31 @@
|
|||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"migration:mysql:generate": "drizzle-kit generate --config ./configs/mysql.config.ts",
|
"migration:mysql:generate": "drizzle-kit generate --config ./configs/mysql.config.ts",
|
||||||
"migration:mysql:run": "drizzle-kit migrate --config ./configs/mysql.config.ts",
|
"migration:mysql:run": "drizzle-kit migrate --config ./configs/mysql.config.ts && pnpm run seed",
|
||||||
"migration:mysql:drop": "drizzle-kit drop --config ./configs/mysql.config.ts",
|
"migration:mysql:drop": "drizzle-kit drop --config ./configs/mysql.config.ts",
|
||||||
"migration:sqlite:generate": "drizzle-kit generate --config ./configs/sqlite.config.ts",
|
"migration:sqlite:generate": "drizzle-kit generate --config ./configs/sqlite.config.ts",
|
||||||
"migration:sqlite:run": "drizzle-kit migrate --config ./configs/sqlite.config.ts",
|
"migration:sqlite:run": "drizzle-kit migrate --config ./configs/sqlite.config.ts && pnpm run seed",
|
||||||
"migration:sqlite:drop": "drizzle-kit drop --config ./configs/sqlite.config.ts",
|
"migration:sqlite:drop": "drizzle-kit drop --config ./configs/sqlite.config.ts",
|
||||||
"push:mysql": "drizzle-kit push --config ./configs/mysql.config.ts",
|
"push:mysql": "drizzle-kit push --config ./configs/mysql.config.ts",
|
||||||
"push:sqlite": "drizzle-kit push --config ./configs/sqlite.config.ts",
|
"push:sqlite": "drizzle-kit push --config ./configs/sqlite.config.ts",
|
||||||
|
"seed": "pnpm with-env tsx ./migrations/run-seed.ts",
|
||||||
"studio": "drizzle-kit studio --config ./configs/sqlite.config.ts",
|
"studio": "drizzle-kit studio --config ./configs/sqlite.config.ts",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"with-env": "dotenv -e ../../.env --"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.37.1",
|
"@auth/core": "^0.37.2",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@testcontainers/mysql": "^10.13.2",
|
"@testcontainers/mysql": "^10.13.2",
|
||||||
"better-sqlite3": "^11.4.0",
|
"better-sqlite3": "^11.5.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"drizzle-kit": "^0.26.2",
|
"drizzle-kit": "^0.26.2",
|
||||||
"drizzle-orm": "^0.35.2",
|
"drizzle-orm": "^0.35.3",
|
||||||
"mysql2": "3.11.3"
|
"mysql2": "3.11.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -51,8 +54,9 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/better-sqlite3": "7.6.11",
|
"@types/better-sqlite3": "7.6.11",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
|
"tsx": "4.19.1",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export const groupMembers = mysqlTable(
|
|||||||
|
|
||||||
export const groups = mysqlTable("group", {
|
export const groups = mysqlTable("group", {
|
||||||
id: varchar("id", { length: 64 }).notNull().primaryKey(),
|
id: varchar("id", { length: 64 }).notNull().primaryKey(),
|
||||||
name: varchar("name", { length: 64 }).notNull(),
|
name: varchar("name", { length: 64 }).unique().notNull(),
|
||||||
ownerId: varchar("owner_id", { length: 64 }).references(() => users.id, {
|
ownerId: varchar("owner_id", { length: 64 }).references(() => users.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const groupMembers = sqliteTable(
|
|||||||
|
|
||||||
export const groups = sqliteTable("group", {
|
export const groups = sqliteTable("group", {
|
||||||
id: text("id").notNull().primaryKey(),
|
id: text("id").notNull().primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").unique().notNull(),
|
||||||
ownerId: text("owner_id").references(() => users.id, {
|
ownerId: text("owner_id").references(() => users.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"postinstall": "tsx ./src/docs/codegen.ts"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
75
packages/definitions/src/docs/codegen.ts
Normal file
75
packages/definitions/src/docs/codegen.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import path, { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { XMLParser } from "fast-xml-parser";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createDocumentationLink } from "./index";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const removeCommonUrl = (url: string) => {
|
||||||
|
return url.replace("https://homarr.dev", "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const sitemapSchema = z.object({
|
||||||
|
urlset: z.object({
|
||||||
|
url: z.array(
|
||||||
|
z.object({
|
||||||
|
loc: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchSitemapAsync = async () => {
|
||||||
|
const response = await fetch(createDocumentationLink("/sitemap.xml"));
|
||||||
|
return await response.text();
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseXml = (sitemapXml: string) => {
|
||||||
|
const parser = new XMLParser();
|
||||||
|
const data: unknown = parser.parse(sitemapXml);
|
||||||
|
const result = sitemapSchema.safeParse(data);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error("Invalid sitemap schema");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapSitemapXmlToPaths = (sitemapData: z.infer<typeof sitemapSchema>) => {
|
||||||
|
return sitemapData.urlset.url.map((url) => removeCommonUrl(url.loc));
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSitemapPathType = (paths: string[]) => {
|
||||||
|
return "export type HomarrDocumentationPath =\n" + paths.map((path) => ` | "${path.replace(/\/$/, "")}"`).join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSitemapTypeFileAsync = async (sitemapPathType: string) => {
|
||||||
|
const content =
|
||||||
|
"// This file is auto-generated by the codegen script\n" +
|
||||||
|
"// it uses the sitemap.xml to generate the HomarrDocumentationPath type\n" +
|
||||||
|
sitemapPathType +
|
||||||
|
";\n";
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(__dirname, "homarr-docs-sitemap.ts"), content);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This script fetches the sitemap.xml and generates the HomarrDocumentationPath type
|
||||||
|
* which is used for typesafe documentation links
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
const main = async () => {
|
||||||
|
const sitemapXml = await fetchSitemapAsync();
|
||||||
|
const sitemapData = parseXml(sitemapXml);
|
||||||
|
const paths = mapSitemapXmlToPaths(sitemapData);
|
||||||
|
// Adding sitemap as it's not in the sitemap.xml and we need it for this file
|
||||||
|
paths.push("/sitemap.xml");
|
||||||
|
const sitemapPathType = createSitemapPathType(paths);
|
||||||
|
await updateSitemapTypeFileAsync(sitemapPathType);
|
||||||
|
};
|
||||||
|
|
||||||
|
void main();
|
||||||
191
packages/definitions/src/docs/homarr-docs-sitemap.ts
Normal file
191
packages/definitions/src/docs/homarr-docs-sitemap.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
// This file is auto-generated by the codegen script
|
||||||
|
// it uses the sitemap.xml to generate the HomarrDocumentationPath type
|
||||||
|
export type HomarrDocumentationPath =
|
||||||
|
| "/about-us"
|
||||||
|
| "/blog"
|
||||||
|
| "/blog/2023/01/11/version0.11"
|
||||||
|
| "/blog/2023/04/16/version0.12-more-widgets"
|
||||||
|
| "/blog/2023/11/10/authentication"
|
||||||
|
| "/blog/2023/12/22/updated-documentation"
|
||||||
|
| "/blog/2024/09/23/version-1.0"
|
||||||
|
| "/blog/archive"
|
||||||
|
| "/blog/authors"
|
||||||
|
| "/blog/authors/ajnart"
|
||||||
|
| "/blog/authors/manuel-rw"
|
||||||
|
| "/blog/authors/meierschlumpf"
|
||||||
|
| "/blog/authors/tagashi"
|
||||||
|
| "/blog/authors/walkx"
|
||||||
|
| "/blog/documentation-migration"
|
||||||
|
| "/blog/tags"
|
||||||
|
| "/blog/tags/authentication"
|
||||||
|
| "/blog/tags/breaking-changes"
|
||||||
|
| "/blog/tags/contributions"
|
||||||
|
| "/blog/tags/design"
|
||||||
|
| "/blog/tags/dnd"
|
||||||
|
| "/blog/tags/docs"
|
||||||
|
| "/blog/tags/documentation"
|
||||||
|
| "/blog/tags/gridstack"
|
||||||
|
| "/blog/tags/homarr"
|
||||||
|
| "/blog/tags/migration"
|
||||||
|
| "/blog/tags/notepad"
|
||||||
|
| "/blog/tags/security"
|
||||||
|
| "/blog/tags/translations"
|
||||||
|
| "/blog/tags/update"
|
||||||
|
| "/blog/tags/version"
|
||||||
|
| "/blog/translations"
|
||||||
|
| "/docs/tags"
|
||||||
|
| "/docs/tags/active-directory"
|
||||||
|
| "/docs/tags/ad-guard"
|
||||||
|
| "/docs/tags/ad-guard-home"
|
||||||
|
| "/docs/tags/administration"
|
||||||
|
| "/docs/tags/advanced"
|
||||||
|
| "/docs/tags/analytics"
|
||||||
|
| "/docs/tags/api"
|
||||||
|
| "/docs/tags/banner"
|
||||||
|
| "/docs/tags/blocking"
|
||||||
|
| "/docs/tags/board"
|
||||||
|
| "/docs/tags/boards"
|
||||||
|
| "/docs/tags/bookmark"
|
||||||
|
| "/docs/tags/caddy"
|
||||||
|
| "/docs/tags/checklist"
|
||||||
|
| "/docs/tags/code"
|
||||||
|
| "/docs/tags/community"
|
||||||
|
| "/docs/tags/configuration"
|
||||||
|
| "/docs/tags/connections"
|
||||||
|
| "/docs/tags/customization"
|
||||||
|
| "/docs/tags/data-sources"
|
||||||
|
| "/docs/tags/developer"
|
||||||
|
| "/docs/tags/development"
|
||||||
|
| "/docs/tags/dns"
|
||||||
|
| "/docs/tags/docker"
|
||||||
|
| "/docs/tags/edit-mode"
|
||||||
|
| "/docs/tags/env"
|
||||||
|
| "/docs/tags/environment-variables"
|
||||||
|
| "/docs/tags/feeds"
|
||||||
|
| "/docs/tags/getting-started"
|
||||||
|
| "/docs/tags/google"
|
||||||
|
| "/docs/tags/grafana"
|
||||||
|
| "/docs/tags/groups"
|
||||||
|
| "/docs/tags/hardware"
|
||||||
|
| "/docs/tags/health"
|
||||||
|
| "/docs/tags/help"
|
||||||
|
| "/docs/tags/icons"
|
||||||
|
| "/docs/tags/iframe"
|
||||||
|
| "/docs/tags/images"
|
||||||
|
| "/docs/tags/installation"
|
||||||
|
| "/docs/tags/integrade"
|
||||||
|
| "/docs/tags/integration"
|
||||||
|
| "/docs/tags/integrations"
|
||||||
|
| "/docs/tags/interface"
|
||||||
|
| "/docs/tags/jellyserr"
|
||||||
|
| "/docs/tags/ldap"
|
||||||
|
| "/docs/tags/links"
|
||||||
|
| "/docs/tags/lists"
|
||||||
|
| "/docs/tags/management"
|
||||||
|
| "/docs/tags/monitoring"
|
||||||
|
| "/docs/tags/news"
|
||||||
|
| "/docs/tags/notebook"
|
||||||
|
| "/docs/tags/notes"
|
||||||
|
| "/docs/tags/oidc"
|
||||||
|
| "/docs/tags/open-media-vault"
|
||||||
|
| "/docs/tags/overseerr"
|
||||||
|
| "/docs/tags/permissions"
|
||||||
|
| "/docs/tags/pi-hole"
|
||||||
|
| "/docs/tags/preferences"
|
||||||
|
| "/docs/tags/programming"
|
||||||
|
| "/docs/tags/proxmox"
|
||||||
|
| "/docs/tags/proxy"
|
||||||
|
| "/docs/tags/roles"
|
||||||
|
| "/docs/tags/rss"
|
||||||
|
| "/docs/tags/search"
|
||||||
|
| "/docs/tags/search-engines"
|
||||||
|
| "/docs/tags/security"
|
||||||
|
| "/docs/tags/seo"
|
||||||
|
| "/docs/tags/server"
|
||||||
|
| "/docs/tags/settings"
|
||||||
|
| "/docs/tags/sinkhole"
|
||||||
|
| "/docs/tags/sso"
|
||||||
|
| "/docs/tags/system"
|
||||||
|
| "/docs/tags/table"
|
||||||
|
| "/docs/tags/technical-documentation"
|
||||||
|
| "/docs/tags/text"
|
||||||
|
| "/docs/tags/theming"
|
||||||
|
| "/docs/tags/traefik"
|
||||||
|
| "/docs/tags/translations"
|
||||||
|
| "/docs/tags/unraid"
|
||||||
|
| "/docs/tags/user"
|
||||||
|
| "/docs/tags/users"
|
||||||
|
| "/docs/tags/variables"
|
||||||
|
| "/docs/tags/widgets"
|
||||||
|
| "/docs/advanced/command-line"
|
||||||
|
| "/docs/advanced/command-line/password-recovery"
|
||||||
|
| "/docs/advanced/configuration/environment-variables"
|
||||||
|
| "/docs/advanced/configuration/keyboard-shortcuts"
|
||||||
|
| "/docs/advanced/configuration/proxies-and-certificates"
|
||||||
|
| "/docs/advanced/customizations/board-customization"
|
||||||
|
| "/docs/advanced/customizations/dark-mode"
|
||||||
|
| "/docs/advanced/customizations/icons"
|
||||||
|
| "/docs/advanced/customizations/user-preferences"
|
||||||
|
| "/docs/advanced/sso"
|
||||||
|
| "/docs/category/advanced"
|
||||||
|
| "/docs/category/getting-started"
|
||||||
|
| "/docs/category/installation"
|
||||||
|
| "/docs/category/installation-1"
|
||||||
|
| "/docs/category/integrations"
|
||||||
|
| "/docs/category/management"
|
||||||
|
| "/docs/category/more"
|
||||||
|
| "/docs/category/widgets"
|
||||||
|
| "/docs/community/developer-guides"
|
||||||
|
| "/docs/community/donate"
|
||||||
|
| "/docs/community/faq"
|
||||||
|
| "/docs/community/get-in-touch"
|
||||||
|
| "/docs/community/license"
|
||||||
|
| "/docs/community/translations"
|
||||||
|
| "/docs/getting-started"
|
||||||
|
| "/docs/getting-started/after-the-installation"
|
||||||
|
| "/docs/getting-started/glossary"
|
||||||
|
| "/docs/getting-started/installation/docker"
|
||||||
|
| "/docs/getting-started/installation/easy-panel"
|
||||||
|
| "/docs/getting-started/installation/home-assistant"
|
||||||
|
| "/docs/getting-started/installation/kubernetes"
|
||||||
|
| "/docs/getting-started/installation/portainer"
|
||||||
|
| "/docs/getting-started/installation/qnap"
|
||||||
|
| "/docs/getting-started/installation/saltbox"
|
||||||
|
| "/docs/getting-started/installation/source"
|
||||||
|
| "/docs/getting-started/installation/synology"
|
||||||
|
| "/docs/getting-started/installation/truenas"
|
||||||
|
| "/docs/getting-started/installation/unraid"
|
||||||
|
| "/docs/integrations/containers"
|
||||||
|
| "/docs/integrations/dns"
|
||||||
|
| "/docs/integrations/hardware"
|
||||||
|
| "/docs/integrations/media-requester"
|
||||||
|
| "/docs/integrations/media-server"
|
||||||
|
| "/docs/integrations/servarr"
|
||||||
|
| "/docs/integrations/torrent"
|
||||||
|
| "/docs/integrations/usenet"
|
||||||
|
| "/docs/management/api"
|
||||||
|
| "/docs/management/boards"
|
||||||
|
| "/docs/management/integrations"
|
||||||
|
| "/docs/management/search-engines"
|
||||||
|
| "/docs/management/settings"
|
||||||
|
| "/docs/management/users"
|
||||||
|
| "/docs/widgets/bookmarks"
|
||||||
|
| "/docs/widgets/calendar-widget"
|
||||||
|
| "/docs/widgets/clock-widget"
|
||||||
|
| "/docs/widgets/dashdot-widget"
|
||||||
|
| "/docs/widgets/dns-hole"
|
||||||
|
| "/docs/widgets/download-speed-widget"
|
||||||
|
| "/docs/widgets/health-monitoring"
|
||||||
|
| "/docs/widgets/home-assistant"
|
||||||
|
| "/docs/widgets/iframe"
|
||||||
|
| "/docs/widgets/indexer-manager"
|
||||||
|
| "/docs/widgets/media-requests"
|
||||||
|
| "/docs/widgets/media-server"
|
||||||
|
| "/docs/widgets/notebook"
|
||||||
|
| "/docs/widgets/rss-widget"
|
||||||
|
| "/docs/widgets/torrent-widget"
|
||||||
|
| "/docs/widgets/usenet-widget"
|
||||||
|
| "/docs/widgets/video"
|
||||||
|
| "/docs/widgets/weather-widget"
|
||||||
|
| ""
|
||||||
|
| "/sitemap.xml";
|
||||||
7
packages/definitions/src/docs/index.ts
Normal file
7
packages/definitions/src/docs/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { HomarrDocumentationPath } from "./homarr-docs-sitemap";
|
||||||
|
|
||||||
|
const documentationBaseUrl = "https://deploy-preview-113--homarr-docs.netlify.app";
|
||||||
|
|
||||||
|
// Please use the method so the path can be checked!
|
||||||
|
export const createDocumentationLink = (path: HomarrDocumentationPath, hashTag?: `#${string}`) =>
|
||||||
|
`${documentationBaseUrl}${path}${hashTag ?? ""}`;
|
||||||
1
packages/definitions/src/group.ts
Normal file
1
packages/definitions/src/group.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const everyoneGroup = "everyone";
|
||||||
@@ -6,3 +6,5 @@ export * from "./permissions";
|
|||||||
export * from "./docker";
|
export * from "./docker";
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
export * from "./user";
|
export * from "./user";
|
||||||
|
export * from "./group";
|
||||||
|
export * from "./docs";
|
||||||
|
|||||||
@@ -24,13 +24,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/form": "^7.13.3"
|
"@mantine/form": "^7.13.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,15 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@jellyfin/sdk": "^0.10.0"
|
"@jellyfin/sdk": "^0.10.0",
|
||||||
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"@types/xml2js": "^0.4.14",
|
||||||
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration"
|
|||||||
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
||||||
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
||||||
import { PiHoleIntegration } from "../pi-hole/pi-hole-integration";
|
import { PiHoleIntegration } from "../pi-hole/pi-hole-integration";
|
||||||
|
import { PlexIntegration } from "../plex/plex-integration";
|
||||||
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
||||||
import type { Integration, IntegrationInput } from "./integration";
|
import type { Integration, IntegrationInput } from "./integration";
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ export const integrationCreators = {
|
|||||||
adGuardHome: AdGuardHomeIntegration,
|
adGuardHome: AdGuardHomeIntegration,
|
||||||
homeAssistant: HomeAssistantIntegration,
|
homeAssistant: HomeAssistantIntegration,
|
||||||
jellyfin: JellyfinIntegration,
|
jellyfin: JellyfinIntegration,
|
||||||
|
plex: PlexIntegration,
|
||||||
sonarr: SonarrIntegration,
|
sonarr: SonarrIntegration,
|
||||||
radarr: RadarrIntegration,
|
radarr: RadarrIntegration,
|
||||||
sabNzbd: SabnzbdIntegration,
|
sabNzbd: SabnzbdIntegration,
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
// General integrations
|
// General integrations
|
||||||
export { AdGuardHomeIntegration } from "./adguard-home/adguard-home-integration";
|
export { AdGuardHomeIntegration } from "./adguard-home/adguard-home-integration";
|
||||||
|
export { DelugeIntegration } from "./download-client/deluge/deluge-integration";
|
||||||
|
export { NzbGetIntegration } from "./download-client/nzbget/nzbget-integration";
|
||||||
|
export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorrent-integration";
|
||||||
|
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
|
||||||
|
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
|
||||||
export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration";
|
export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration";
|
||||||
export { JellyfinIntegration } from "./jellyfin/jellyfin-integration";
|
|
||||||
export { DownloadClientIntegration } from "./interfaces/downloads/download-client-integration";
|
export { DownloadClientIntegration } from "./interfaces/downloads/download-client-integration";
|
||||||
|
export { JellyfinIntegration } from "./jellyfin/jellyfin-integration";
|
||||||
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
|
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
|
||||||
export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration";
|
export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration";
|
||||||
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
|
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
|
||||||
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
|
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
|
||||||
export { OverseerrIntegration } from "./overseerr/overseerr-integration";
|
export { OverseerrIntegration } from "./overseerr/overseerr-integration";
|
||||||
export { PiHoleIntegration } from "./pi-hole/pi-hole-integration";
|
export { PiHoleIntegration } from "./pi-hole/pi-hole-integration";
|
||||||
|
export { PlexIntegration } from "./plex/plex-integration";
|
||||||
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
||||||
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
|
|
||||||
export { NzbGetIntegration } from "./download-client/nzbget/nzbget-integration";
|
|
||||||
export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorrent-integration";
|
|
||||||
export { DelugeIntegration } from "./download-client/deluge/deluge-integration";
|
|
||||||
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
|
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
export type { IntegrationInput } from "./base/integration";
|
||||||
|
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
|
||||||
|
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
||||||
|
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
||||||
export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring";
|
export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring";
|
||||||
export { MediaRequestStatus } from "./interfaces/media-requests/media-request";
|
export { MediaRequestStatus } from "./interfaces/media-requests/media-request";
|
||||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request";
|
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request";
|
||||||
export type { StreamSession } from "./interfaces/media-server/session";
|
export type { StreamSession } from "./interfaces/media-server/session";
|
||||||
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
|
||||||
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
|
||||||
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
|
|
||||||
export type { IntegrationInput } from "./base/integration";
|
|
||||||
|
|
||||||
// Schemas
|
// Schemas
|
||||||
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
|
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
|
||||||
|
|||||||
37
packages/integrations/src/plex/interface.ts
Normal file
37
packages/integrations/src/plex/interface.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
interface MediaContainer {
|
||||||
|
Video?: Session[];
|
||||||
|
Track?: Session[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Session {
|
||||||
|
User?: {
|
||||||
|
$: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
thumb?: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
Player?: {
|
||||||
|
$: {
|
||||||
|
product: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
Session?: {
|
||||||
|
$: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
$: {
|
||||||
|
grandparentTitle?: string;
|
||||||
|
parentTitle?: string;
|
||||||
|
title?: string;
|
||||||
|
index?: number;
|
||||||
|
type: string;
|
||||||
|
live?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlexResponse {
|
||||||
|
MediaContainer: MediaContainer;
|
||||||
|
}
|
||||||
103
packages/integrations/src/plex/plex-integration.ts
Normal file
103
packages/integrations/src/plex/plex-integration.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { parseStringPromise } from "xml2js";
|
||||||
|
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
import { Integration } from "../base/integration";
|
||||||
|
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||||
|
import type { StreamSession } from "../interfaces/media-server/session";
|
||||||
|
import type { PlexResponse } from "./interface";
|
||||||
|
|
||||||
|
export class PlexIntegration extends Integration {
|
||||||
|
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
||||||
|
const token = super.getSecretValue("apiKey");
|
||||||
|
|
||||||
|
const response = await fetch(`${this.integration.url}/status/sessions`, {
|
||||||
|
headers: {
|
||||||
|
"X-Plex-Token": token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await response.text();
|
||||||
|
// convert xml response to objects, as there is no JSON api
|
||||||
|
const data = await PlexIntegration.parseXml<PlexResponse>(body);
|
||||||
|
const mediaContainer = data.MediaContainer;
|
||||||
|
const mediaElements = [mediaContainer.Video ?? [], mediaContainer.Track ?? []].flat();
|
||||||
|
|
||||||
|
// no sessions are open or available
|
||||||
|
if (mediaElements.length === 0) {
|
||||||
|
logger.info("No active video sessions found in MediaContainer");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const medias = mediaElements
|
||||||
|
.map((mediaElement): StreamSession | undefined => {
|
||||||
|
const userElement = mediaElement.User ? mediaElement.User[0] : undefined;
|
||||||
|
const playerElement = mediaElement.Player ? mediaElement.Player[0] : undefined;
|
||||||
|
const sessionElement = mediaElement.Session ? mediaElement.Session[0] : undefined;
|
||||||
|
|
||||||
|
if (!playerElement) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId: sessionElement?.$.id ?? "unknown",
|
||||||
|
sessionName: `${playerElement.$.product} (${playerElement.$.title})`,
|
||||||
|
user: {
|
||||||
|
userId: userElement?.$.id ?? "Anonymous",
|
||||||
|
username: userElement?.$.title ?? "Anonymous",
|
||||||
|
profilePictureUrl: userElement?.$.thumb ?? null,
|
||||||
|
},
|
||||||
|
currentlyPlaying: {
|
||||||
|
type: mediaElement.$.live === "1" ? "tv" : PlexIntegration.getCurrentlyPlayingType(mediaElement.$.type),
|
||||||
|
name: mediaElement.$.grandparentTitle ?? mediaElement.$.title ?? "Unknown",
|
||||||
|
seasonName: mediaElement.$.parentTitle,
|
||||||
|
episodeName: mediaElement.$.title ?? null,
|
||||||
|
albumName: mediaElement.$.type === "track" ? (mediaElement.$.parentTitle ?? null) : null,
|
||||||
|
episodeCount: mediaElement.$.index ?? null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((session): session is StreamSession => session !== undefined);
|
||||||
|
|
||||||
|
return medias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testConnectionAsync(): Promise<void> {
|
||||||
|
const token = super.getSecretValue("apiKey");
|
||||||
|
|
||||||
|
await super.handleTestConnectionResponseAsync({
|
||||||
|
queryFunctionAsync: async () => {
|
||||||
|
return await fetch(this.integration.url, {
|
||||||
|
headers: {
|
||||||
|
"X-Plex-Token": token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleResponseAsync: async (response) => {
|
||||||
|
try {
|
||||||
|
const result = await response.text();
|
||||||
|
await PlexIntegration.parseXml<PlexResponse>(result);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
throw new IntegrationTestConnectionError("invalidCredentials");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static parseXml<T>(xml: string): Promise<T> {
|
||||||
|
return parseStringPromise(xml) as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCurrentlyPlayingType(type: string): NonNullable<StreamSession["currentlyPlaying"]>["type"] {
|
||||||
|
switch (type) {
|
||||||
|
case "movie":
|
||||||
|
return "movie";
|
||||||
|
case "episode":
|
||||||
|
return "video";
|
||||||
|
case "track":
|
||||||
|
return "audio";
|
||||||
|
default:
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,17 +30,17 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.13.3",
|
"@mantine/core": "^7.13.4",
|
||||||
"@tabler/icons-react": "^3.19.0",
|
"@tabler/icons-react": "^3.20.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"next": "^14.2.15",
|
"next": "^14.2.16",
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
@@ -24,15 +24,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.13.3",
|
"@mantine/core": "^7.13.4",
|
||||||
"@mantine/hooks": "^7.13.3",
|
"@mantine/hooks": "^7.13.4",
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,14 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/notifications": "^7.13.3",
|
"@mantine/notifications": "^7.13.4",
|
||||||
"@tabler/icons-react": "^3.19.0"
|
"@tabler/icons-react": "^3.20.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type OldmarrRssDefinition = CommonOldmarrWidgetDefinition<
|
|||||||
"rss",
|
"rss",
|
||||||
{
|
{
|
||||||
rssFeedUrl: string[];
|
rssFeedUrl: string[];
|
||||||
|
enableRtl: boolean;
|
||||||
refreshInterval: number;
|
refreshInterval: number;
|
||||||
dangerousAllowSanitizedItemContent: boolean;
|
dangerousAllowSanitizedItemContent: boolean;
|
||||||
textLinesClamp: number;
|
textLinesClamp: number;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const optionMapping: OptionMapping = {
|
|||||||
},
|
},
|
||||||
rssFeed: {
|
rssFeed: {
|
||||||
feedUrls: (oldOptions) => oldOptions.rssFeedUrl,
|
feedUrls: (oldOptions) => oldOptions.rssFeedUrl,
|
||||||
|
enableRtl: (oldOptions) => oldOptions.enableRtl,
|
||||||
maximumAmountPosts: (oldOptions) => oldOptions.maximumAmountOfPosts,
|
maximumAmountPosts: (oldOptions) => oldOptions.maximumAmountOfPosts,
|
||||||
textLinesClamp: (oldOptions) => oldOptions.textLinesClamp,
|
textLinesClamp: (oldOptions) => oldOptions.textLinesClamp,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,12 +30,12 @@
|
|||||||
"@homarr/modals-collection": "workspace:^0.1.0",
|
"@homarr/modals-collection": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.13.3",
|
"@mantine/core": "^7.13.4",
|
||||||
"@mantine/hooks": "^7.13.3",
|
"@mantine/hooks": "^7.13.4",
|
||||||
"@mantine/spotlight": "^7.13.3",
|
"@mantine/spotlight": "^7.13.4",
|
||||||
"@tabler/icons-react": "^3.19.0",
|
"@tabler/icons-react": "^3.20.0",
|
||||||
"jotai": "^2.10.1",
|
"jotai": "^2.10.1",
|
||||||
"next": "^14.2.15",
|
"next": "^14.2.16",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"use-deep-compare-effect": "^1.8.1"
|
"use-deep-compare-effect": "^1.8.1"
|
||||||
},
|
},
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Group, Kbd, Text } from "@mantine/core";
|
import { Group, Kbd, Text } from "@mantine/core";
|
||||||
import { IconBook2, IconBrandDiscord, IconBrandGithub } from "@tabler/icons-react";
|
import { IconBook2, IconBrandDiscord, IconBrandGithub } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { createDocumentationLink } from "@homarr/definitions";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { createGroup } from "../lib/group";
|
import { createGroup } from "../lib/group";
|
||||||
@@ -45,7 +46,7 @@ const helpMode = {
|
|||||||
{
|
{
|
||||||
label: t("documentation.label"),
|
label: t("documentation.label"),
|
||||||
icon: IconBook2,
|
icon: IconBook2,
|
||||||
href: "https://homarr.dev/docs/getting-started/",
|
href: createDocumentationLink("/docs/getting-started"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("submitIssue.label"),
|
label: t("submitIssue.label"),
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,6 +249,9 @@ export default {
|
|||||||
mixed: "Some members are from external providers and cannot be managed here",
|
mixed: "Some members are from external providers and cannot be managed here",
|
||||||
external: "All members are from external providers and cannot be managed here",
|
external: "All members are from external providers and cannot be managed here",
|
||||||
},
|
},
|
||||||
|
reservedNotice: {
|
||||||
|
message: "This group is reserved for system use and restricts some actions. {checkoutDocs}",
|
||||||
|
},
|
||||||
action: {
|
action: {
|
||||||
create: {
|
create: {
|
||||||
label: "New group",
|
label: "New group",
|
||||||
@@ -1351,6 +1354,9 @@ export default {
|
|||||||
feedUrls: {
|
feedUrls: {
|
||||||
label: "Feed URLs",
|
label: "Feed URLs",
|
||||||
},
|
},
|
||||||
|
enableRtl: {
|
||||||
|
label: "Enable RTL",
|
||||||
|
},
|
||||||
textLinesClamp: {
|
textLinesClamp: {
|
||||||
label: "Description line clamp",
|
label: "Description line clamp",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,12 +28,12 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.13.3",
|
"@mantine/core": "^7.13.4",
|
||||||
"@mantine/dates": "^7.13.3",
|
"@mantine/dates": "^7.13.4",
|
||||||
"@mantine/hooks": "^7.13.3",
|
"@mantine/hooks": "^7.13.4",
|
||||||
"@tabler/icons-react": "^3.19.0",
|
"@tabler/icons-react": "^3.20.0",
|
||||||
"mantine-react-table": "2.0.0-beta.7",
|
"mantine-react-table": "2.0.0-beta.7",
|
||||||
"next": "^14.2.15",
|
"next": "^14.2.16",
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/css-modules": "^1.0.5",
|
"@types/css-modules": "^1.0.5",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { groupPermissionKeys } from "@homarr/definitions";
|
import { everyoneGroup, groupPermissionKeys } from "@homarr/definitions";
|
||||||
|
|
||||||
import { byIdSchema } from "./common";
|
import { byIdSchema } from "./common";
|
||||||
import { zodEnumFromArray } from "./enums";
|
import { zodEnumFromArray } from "./enums";
|
||||||
|
|
||||||
const createSchema = z.object({
|
const createSchema = z.object({
|
||||||
name: z.string().trim().min(1).max(64),
|
name: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.max(64)
|
||||||
|
.refine((value) => value !== everyoneGroup, {
|
||||||
|
message: "'everyone' is a reserved group name",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateSchema = createSchema.merge(byIdSchema);
|
const updateSchema = createSchema.merge(byIdSchema);
|
||||||
|
|||||||
@@ -38,37 +38,37 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.13.3",
|
"@mantine/core": "^7.13.4",
|
||||||
"@mantine/hooks": "^7.13.3",
|
"@mantine/hooks": "^7.13.4",
|
||||||
"@tabler/icons-react": "^3.19.0",
|
"@tabler/icons-react": "^3.20.0",
|
||||||
"@tiptap/extension-color": "2.8.0",
|
"@tiptap/extension-color": "2.9.1",
|
||||||
"@tiptap/extension-highlight": "2.8.0",
|
"@tiptap/extension-highlight": "2.9.1",
|
||||||
"@tiptap/extension-image": "2.8.0",
|
"@tiptap/extension-image": "2.9.1",
|
||||||
"@tiptap/extension-link": "^2.8.0",
|
"@tiptap/extension-link": "^2.9.1",
|
||||||
"@tiptap/extension-table": "2.8.0",
|
"@tiptap/extension-table": "2.9.1",
|
||||||
"@tiptap/extension-table-cell": "2.8.0",
|
"@tiptap/extension-table-cell": "2.9.1",
|
||||||
"@tiptap/extension-table-header": "2.8.0",
|
"@tiptap/extension-table-header": "2.9.1",
|
||||||
"@tiptap/extension-table-row": "2.8.0",
|
"@tiptap/extension-table-row": "2.9.1",
|
||||||
"@tiptap/extension-task-item": "2.8.0",
|
"@tiptap/extension-task-item": "2.9.1",
|
||||||
"@tiptap/extension-task-list": "2.8.0",
|
"@tiptap/extension-task-list": "2.9.1",
|
||||||
"@tiptap/extension-text-align": "2.8.0",
|
"@tiptap/extension-text-align": "2.9.1",
|
||||||
"@tiptap/extension-text-style": "2.8.0",
|
"@tiptap/extension-text-style": "2.9.1",
|
||||||
"@tiptap/extension-underline": "2.8.0",
|
"@tiptap/extension-underline": "2.9.1",
|
||||||
"@tiptap/react": "^2.8.0",
|
"@tiptap/react": "^2.9.1",
|
||||||
"@tiptap/starter-kit": "^2.8.0",
|
"@tiptap/starter-kit": "^2.9.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"mantine-react-table": "2.0.0-beta.7",
|
"mantine-react-table": "2.0.0-beta.7",
|
||||||
"next": "^14.2.15",
|
"next": "^14.2.16",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"video.js": "^8.18.1"
|
"video.js": "^8.19.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/video.js": "^7.3.58",
|
"@types/video.js": "^7.3.58",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
|
|||||||
}
|
}
|
||||||
const newData = prevData.map((item) =>
|
const newData = prevData.map((item) =>
|
||||||
item.integrationId === data.integrationId
|
item.integrationId === data.integrationId
|
||||||
? { ...item, healthInfo: data.healthInfo, timestamp: new Date(0) }
|
? { ...item, healthInfo: data.healthInfo, timestamp: data.timestamp }
|
||||||
: item,
|
: item,
|
||||||
);
|
);
|
||||||
return newData.filter(
|
return newData.filter(
|
||||||
@@ -323,19 +323,21 @@ interface SmartData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const matchFileSystemAndSmart = (fileSystems: FileSystem[], smartData: SmartData[]) => {
|
export const matchFileSystemAndSmart = (fileSystems: FileSystem[], smartData: SmartData[]) => {
|
||||||
return fileSystems.map((fileSystem) => {
|
return fileSystems
|
||||||
const baseDeviceName = fileSystem.deviceName.replace(/[0-9]+$/, "");
|
.map((fileSystem) => {
|
||||||
const smartDisk = smartData.find((smart) => smart.deviceName === baseDeviceName);
|
const baseDeviceName = fileSystem.deviceName.replace(/[0-9]+$/, "");
|
||||||
|
const smartDisk = smartData.find((smart) => smart.deviceName === baseDeviceName);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deviceName: smartDisk?.deviceName ?? fileSystem.deviceName,
|
deviceName: smartDisk?.deviceName ?? fileSystem.deviceName,
|
||||||
used: fileSystem.used,
|
used: fileSystem.used,
|
||||||
available: fileSystem.available,
|
available: fileSystem.available,
|
||||||
percentage: fileSystem.percentage,
|
percentage: fileSystem.percentage,
|
||||||
temperature: smartDisk?.temperature ?? 0,
|
temperature: smartDisk?.temperature ?? 0,
|
||||||
overallStatus: smartDisk?.overallStatus ?? "",
|
overallStatus: smartDisk?.overallStatus ?? "",
|
||||||
};
|
};
|
||||||
});
|
})
|
||||||
|
.sort((fileSystemA, fileSystemB) => fileSystemA.deviceName.localeCompare(fileSystemB.deviceName));
|
||||||
};
|
};
|
||||||
|
|
||||||
const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => {
|
const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => {
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ import { createWidgetDefinition } from "../definition";
|
|||||||
export const { componentLoader, definition } = createWidgetDefinition("mediaServer", {
|
export const { componentLoader, definition } = createWidgetDefinition("mediaServer", {
|
||||||
icon: IconVideo,
|
icon: IconVideo,
|
||||||
options: {},
|
options: {},
|
||||||
supportedIntegrations: ["jellyfin"],
|
supportedIntegrations: ["jellyfin", "plex"],
|
||||||
}).withDynamicImport(() => import("./component"));
|
}).withDynamicImport(() => import("./component"));
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { Card, Flex, Group, Image, ScrollArea, Stack, Text } from "@mantine/core";
|
import { Card, Flex, Group, Image, ScrollArea, Stack, Text } from "@mantine/core";
|
||||||
import { IconClock } from "@tabler/icons-react";
|
import { IconClock } from "@tabler/icons-react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
@@ -37,6 +36,8 @@ export default function RssFeed({ options, itemId }: WidgetComponentProps<"rssFe
|
|||||||
})
|
})
|
||||||
.slice(0, options.maximumAmountPosts as number);
|
.slice(0, options.maximumAmountPosts as number);
|
||||||
|
|
||||||
|
const languageDir = options.enableRtl ? "RTL" : "LTR";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="scroll-area-w100" w="100%" p="4cqmin">
|
<ScrollArea className="scroll-area-w100" w="100%" p="4cqmin">
|
||||||
<Stack w={"100%"} gap="4cqmin">
|
<Stack w={"100%"} gap="4cqmin">
|
||||||
@@ -56,12 +57,13 @@ export default function RssFeed({ options, itemId }: WidgetComponentProps<"rssFe
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Flex gap="2.5cqmin" direction="column" w="100%">
|
<Flex gap="2.5cqmin" direction="column" w="100%">
|
||||||
<Text fz="4cqmin" lh="5cqmin" lineClamp={2}>
|
<Text dir={languageDir} fz="4cqmin" lh="5cqmin" lineClamp={2}>
|
||||||
{feedEntry.title}
|
{feedEntry.title}
|
||||||
</Text>
|
</Text>
|
||||||
{feedEntry.description && (
|
{feedEntry.description && (
|
||||||
<Text
|
<Text
|
||||||
className={feedEntry.description}
|
className={feedEntry.description}
|
||||||
|
dir={languageDir}
|
||||||
c="dimmed"
|
c="dimmed"
|
||||||
size="3.5cqmin"
|
size="3.5cqmin"
|
||||||
lineClamp={options.textLinesClamp as number}
|
lineClamp={options.textLinesClamp as number}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ export const { definition, componentLoader } = createWidgetDefinition("rssFeed",
|
|||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
validate: z.string().url(),
|
validate: z.string().url(),
|
||||||
}),
|
}),
|
||||||
|
enableRtl: factory.switch({
|
||||||
|
defaultValue: false,
|
||||||
|
}),
|
||||||
textLinesClamp: factory.number({
|
textLinesClamp: factory.number({
|
||||||
defaultValue: 5,
|
defaultValue: 5,
|
||||||
validate: z.number().min(1).max(50),
|
validate: z.number().min(1).max(50),
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import classes from "./component.module.css";
|
|||||||
|
|
||||||
import "video.js/dist/video-js.css";
|
import "video.js/dist/video-js.css";
|
||||||
|
|
||||||
|
import { createDocumentationLink } from "@homarr/definitions";
|
||||||
|
|
||||||
export default function VideoWidget({ options }: WidgetComponentProps<"video">) {
|
export default function VideoWidget({ options }: WidgetComponentProps<"video">) {
|
||||||
if (options.feedUrl.trim() === "") {
|
if (options.feedUrl.trim() === "") {
|
||||||
return <NoUrl />;
|
return <NoUrl />;
|
||||||
@@ -46,7 +48,7 @@ const ForYoutubeUseIframe = () => {
|
|||||||
<Stack align="center" gap="xs">
|
<Stack align="center" gap="xs">
|
||||||
<IconBrandYoutube />
|
<IconBrandYoutube />
|
||||||
<Title order={4}>{t("widget.video.error.forYoutubeUseIframe")}</Title>
|
<Title order={4}>{t("widget.video.error.forYoutubeUseIframe")}</Title>
|
||||||
<Anchor href="https://homarr.dev/docs/widgets/iframe/">{t("common.action.checkoutDocs")}</Anchor>
|
<Anchor href={createDocumentationLink("/docs/widgets/iframe")}>{t("common.action.checkoutDocs")}</Anchor>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
|
|||||||
2142
pnpm-lock.yaml
generated
2142
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@ export default tseslint.config(
|
|||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
min: 3,
|
min: 3,
|
||||||
exceptions: ["_", "i", "z", "t", "id", "db"], // _ for unused variables, i for index, z for zod, t for translation
|
exceptions: ["_", "i", "z", "t", "id", "db", "fs"], // _ for unused variables, i for index, z for zod, t for translation
|
||||||
properties: "never", // This allows for example the use of <Grid.Col span={{ sm: 12, md: 6 }}> as sm and md would be too short
|
properties: "never", // This allows for example the use of <Grid.Col span={{ sm: 12, md: 6 }}> as sm and md would be too short
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -80,6 +80,11 @@ export default tseslint.config(
|
|||||||
"VariableDeclarator[init.type=/FunctionExpression$/][init.async=true][id.name=/^[a-z].*$/][id.name!=/Async$/]",
|
"VariableDeclarator[init.type=/FunctionExpression$/][init.async=true][id.name=/^[a-z].*$/][id.name!=/Async$/]",
|
||||||
message: "Async function name must end in 'Async' (variable declarator)",
|
message: "Async function name must end in 'Async' (variable declarator)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// \\u002F is the unicode escape for / and is used because of https://github.com/estools/esquery/issues/68
|
||||||
|
selector: "Literal[value=/^https:\\u002F\\u002Fhomarr\\.dev\\u002F.*$/]",
|
||||||
|
message: "Links to 'https://homarr.dev/' should be used with createDocumentationLink method",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,19 +16,19 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/eslint-plugin-next": "^14.2.15",
|
"@next/eslint-plugin-next": "^14.2.16",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-config-turbo": "^2.1.3",
|
"eslint-config-turbo": "^2.2.3",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.0",
|
"eslint-plugin-jsx-a11y": "^6.10.1",
|
||||||
"eslint-plugin-react": "^7.37.1",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"typescript-eslint": "^8.10.0"
|
"typescript-eslint": "^8.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
Reference in New Issue
Block a user