feat: add everyone group (#1322)

* feat: add everyone group through seed

* feat: add reserved group name check in group router actions

* feat: improve user interface for everyone group

* fix: reserved group alert is a server component

* feat: add all users to everyone group

* chore: update lockfile

* fix: format issues

* fix: lint issues

* fix: lint format issues

* test: add unit tests for everyone group

* refactor: add codegen for documentation urls by sitemap

* refactor: change group query to count

* chore: remove migrations temporarily

* chore: add migrations again

* chore: add lint rule to prevent usage of raw documentation links

* fix: format issues
This commit is contained in:
Meier Lukas
2024-10-21 17:23:51 +02:00
committed by GitHub
parent 654880d7e4
commit 2f1c800844
38 changed files with 3900 additions and 139 deletions

View File

@@ -25,6 +25,7 @@ import {
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
import { createDocumentationLink } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { MainHeader } from "~/components/layout/header";
@@ -124,7 +125,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
{
label: t("items.help.items.documentation"),
icon: IconBook2,
href: "https://homarr.dev/docs/getting-started/",
href: createDocumentationLink("/docs/getting-started"),
external: true,
},
{

View File

@@ -15,9 +15,10 @@ interface RenameGroupFormProps {
id: string;
name: string;
};
disabled?: boolean;
}
export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
const form = useZodForm(validation.group.update.pick({ name: true }), {
@@ -28,6 +29,9 @@ export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
const handleSubmit = useCallback(
(values: FormType) => {
if (disabled) {
return;
}
mutate(
{
...values,
@@ -60,13 +64,15 @@ export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<TextInput label={t("group.field.name")} {...form.getInputProps("name")} />
<TextInput label={t("group.field.name")} {...form.getInputProps("name")} disabled={disabled} />
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
{!disabled && (
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
)}
</Stack>
</form>
);

View File

@@ -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>
);
};

View File

@@ -6,9 +6,11 @@ import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { env } from "@homarr/auth/env.mjs";
import { isProviderEnabled } from "@homarr/auth/server";
import { everyoneGroup } from "@homarr/definitions";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { SearchInput, UserAvatar } from "@homarr/ui";
import { ReservedGroupAlert } from "../_reserved-group-alert";
import { AddGroupMember } from "./_add-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 tMembers = await getScopedI18n("management.page.group.setting.members");
const group = await api.group.getById({ id: params.id });
const isReserved = group.name === everyoneGroup;
const filteredMembers = searchParams.search
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -41,15 +44,19 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
<Stack>
<Title>{tMembers("title")}</Title>
{providerTypes !== "credentials" && (
<Alert variant="light" color="yellow" icon={<IconExclamationCircle size="1rem" stroke={1.5} />}>
{t(`group.memberNotice.${providerTypes}`)}
</Alert>
{isReserved ? (
<ReservedGroupAlert />
) : (
providerTypes !== "credentials" && (
<Alert variant="light" color="yellow" icon={<IconExclamationCircle size="1rem" stroke={1.5} />}>
{t(`group.memberNotice.${providerTypes}`)}
</Alert>
)
)}
<Group justify="space-between">
<SearchInput placeholder={`${tMembers("search")}...`} defaultValue={searchParams.search} />
{isProviderEnabled("credentials") && (
{isProviderEnabled("credentials") && !isReserved && (
<AddGroupMember groupId={group.id} presentUserIds={group.members.map((member) => member.id)} />
)}
</Group>
@@ -63,7 +70,7 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
<Table striped highlightOnHover>
<TableTbody>
{filteredMembers.map((member) => (
<Row key={group.id} member={member} groupId={group.id} />
<Row key={group.id} member={member} groupId={group.id} disabled={isReserved} />
))}
</TableTbody>
</Table>
@@ -74,9 +81,10 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD
interface RowProps {
member: RouterOutputs["group"]["getById"]["members"][number];
groupId: string;
disabled?: boolean;
}
const Row = ({ member, groupId }: RowProps) => {
const Row = ({ member, groupId, disabled }: RowProps) => {
return (
<TableTr>
<TableTd>
@@ -88,7 +96,7 @@ const Row = ({ member, groupId }: RowProps) => {
</Group>
</TableTd>
<TableTd w={100}>
{member.provider === "credentials" && <RemoveGroupMember user={member} groupId={groupId} />}
{member.provider === "credentials" && !disabled && <RemoveGroupMember user={member} groupId={groupId} />}
</TableTd>
</TableTr>
);

View File

@@ -1,11 +1,13 @@
import { Stack, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { everyoneGroup } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { DeleteGroup } from "./_delete-group";
import { RenameGroupForm } from "./_rename-group-form";
import { ReservedGroupAlert } from "./_reserved-group-alert";
import { TransferGroupOwnership } from "./_transfer-group-ownership";
interface GroupsDetailPageProps {
@@ -18,26 +20,31 @@ export default async function GroupsDetailPage({ params }: GroupsDetailPageProps
const group = await api.group.getById({ id: params.id });
const tGeneral = await getScopedI18n("management.page.group.setting.general");
const tGroupAction = await getScopedI18n("group.action");
const isReserved = group.name === everyoneGroup;
return (
<Stack>
<Title>{tGeneral("title")}</Title>
<RenameGroupForm group={group} />
{isReserved && <ReservedGroupAlert />}
<DangerZoneRoot>
<DangerZoneItem
label={tGroupAction("transfer.label")}
description={tGroupAction("transfer.description")}
action={<TransferGroupOwnership group={group} />}
/>
<RenameGroupForm group={group} disabled={isReserved} />
<DangerZoneItem
label={tGroupAction("delete.label")}
description={tGroupAction("delete.description")}
action={<DeleteGroup group={group} />}
/>
</DangerZoneRoot>
{!isReserved && (
<DangerZoneRoot>
<DangerZoneItem
label={tGroupAction("transfer.label")}
description={tGroupAction("transfer.description")}
action={<TransferGroupOwnership group={group} />}
/>
<DangerZoneItem
label={tGroupAction("delete.label")}
description={tGroupAction("delete.description")}
action={<DeleteGroup group={group} />}
/>
</DangerZoneRoot>
)}
</Stack>
);
}