Files
homarr/apps/nextjs/src/components/access/access-settings.tsx
Meier Lukas 408cdeb5c3 feat: add integration access settings (#725)
* feat: add integration access settings

* fix: typecheck and test issues

* fix: test timeout

* chore: address pull request feedback

* chore: add throw if action forbidden for integration permissions

* fix: unable to create new migrations because of duplicate prevId in sqlite snapshots

* chore: add sqlite migration for integration permissions

* test: add unit tests for integration access

* test: add permission checks to integration router tests

* test: add unit test for integration permissions

* chore: add mysql migration

* fix: format issues
2024-07-08 00:00:37 +02:00

190 lines
5.2 KiB
TypeScript

import { useState } from "react";
import { Group, Stack, Tabs } from "@mantine/core";
import { IconUser, IconUserDown, IconUsersGroup } from "@tabler/icons-react";
import type { GroupPermissionKey } from "@homarr/definitions";
import { useScopedI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import { CountBadge } from "@homarr/ui";
import { AccessProvider } from "./context";
import type { AccessFormType } from "./form";
import { GroupAccessForm } from "./group-access-form";
import { InheritAccessTable } from "./inherit-access-table";
import { UsersAccessForm } from "./user-access-form";
interface GroupAccessPermission<TPermission extends string> {
permission: TPermission;
group: {
id: string;
name: string;
};
}
interface UserAccessPermission<TPermission extends string> {
permission: TPermission;
user: {
name: string | null;
image: string | null;
id: string;
};
}
interface SimpleMutation<TPermission extends string> {
mutate: (
props: { entityId: string; permissions: { principalId: string; permission: TPermission }[] },
options: { onSuccess: () => void },
) => void;
isPending: boolean;
}
export interface AccessQueryData<TPermission extends string> {
inherited: GroupAccessPermission<GroupPermissionKey>[];
groups: GroupAccessPermission<TPermission>[];
users: UserAccessPermission<TPermission>[];
}
interface Props<TPermission extends string> {
permission: {
items: readonly TPermission[];
default: TPermission;
icons: Record<TPermission, TablerIcon>;
groupPermissionMapping: Record<TPermission, GroupPermissionKey>;
fullAccessGroupPermission: GroupPermissionKey;
};
query: {
data: AccessQueryData<TPermission>;
invalidate: () => Promise<void>;
};
groupsMutation: SimpleMutation<TPermission>;
usersMutation: SimpleMutation<TPermission>;
entity: {
id: string;
ownerId: string | null;
owner: {
id: string;
name: string | null;
image: string | null;
} | null;
};
translate: (key: TPermission) => string;
}
export const AccessSettings = <TPermission extends string>({
permission,
query,
groupsMutation,
usersMutation,
entity,
translate,
}: Props<TPermission>) => {
const [counts, setCounts] = useState({
user: query.data.users.length + (entity.owner ? 1 : 0),
group: query.data.groups.length,
});
const handleGroupSubmit = (values: AccessFormType<TPermission>) => {
groupsMutation.mutate(
{
entityId: entity.id,
permissions: values.items,
},
{
onSuccess() {
void query.invalidate();
},
},
);
};
const handleUserSubmit = (values: AccessFormType<TPermission>) => {
usersMutation.mutate(
{
entityId: entity.id,
permissions: values.items,
},
{
onSuccess() {
void query.invalidate();
},
},
);
};
return (
<AccessProvider<TPermission>
defaultPermission={permission.default}
icons={permission.icons}
permissions={permission.items}
translate={translate}
>
<Stack>
<Tabs color="red" defaultValue="user">
<Tabs.List grow>
<TabItem value="user" count={counts.user} icon={IconUser} />
<TabItem value="group" count={counts.group} icon={IconUsersGroup} />
<TabItem value="inherited" count={query.data.inherited.length} icon={IconUserDown} />
</Tabs.List>
<Tabs.Panel value="user">
<UsersAccessForm<TPermission>
entity={entity}
accessQueryData={query.data}
handleCountChange={(callback) =>
setCounts(({ user, ...others }) => ({
user: callback(user),
...others,
}))
}
handleSubmit={handleUserSubmit}
isPending={usersMutation.isPending}
/>
</Tabs.Panel>
<Tabs.Panel value="group">
<GroupAccessForm<TPermission>
accessQueryData={query.data}
handleCountChange={(callback) =>
setCounts(({ group, ...others }) => ({
group: callback(group),
...others,
}))
}
handleSubmit={handleGroupSubmit}
isPending={groupsMutation.isPending}
/>
</Tabs.Panel>
<Tabs.Panel value="inherited">
<InheritAccessTable<TPermission>
accessQueryData={query.data}
fullAccessGroupPermission={permission.fullAccessGroupPermission}
mapPermissions={permission.groupPermissionMapping}
/>
</Tabs.Panel>
</Tabs>
</Stack>
</AccessProvider>
);
};
interface TabItemProps {
value: "user" | "group" | "inherited";
count: number;
icon: TablerIcon;
}
const TabItem = ({ value, icon: Icon, count }: TabItemProps) => {
const t = useScopedI18n("permission");
return (
<Tabs.Tab value={value} leftSection={<Icon stroke={1.5} size={16} />}>
<Group gap="sm">
{t(`tab.${value}`)}
<CountBadge count={count} />
</Group>
</Tabs.Tab>
);
};