Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Teams Management System #882

Draft
wants to merge 7 commits into
base: canary
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { PermissionFormFields } from "@/components/shared/permission-form-fields";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Form } from "@/components/ui/form";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import type { TeamRole } from "@dokploy/server/db/schema/team-schema";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { teamPermissionSchema, type TeamPermissions } from "@/components/shared/shared-permissions";

interface Props {
teamId: string;
userId: string;
role: TeamRole;
}

export const AddMemberPermissions = ({ teamId, userId, role }: Props) => {
const utils = api.useContext();
const { data: member, refetch } = api.team.getMemberPermissions.useQuery(
{ teamId, userId },
{ enabled: !!teamId && !!userId },
);

const { mutateAsync, isError, error, isLoading } =
api.team.updateMemberPermissions.useMutation({
onSuccess: () => {
utils.team.getMemberPermissions.invalidate({ teamId, userId });
utils.team.byId.invalidate({ teamId });
},
});

const form = useForm<TeamPermissions>({
resolver: zodResolver(teamPermissionSchema),
});

useEffect(() => {
if (member) {
form.reset({
canManageTeam: member.canManageTeam,
canInviteMembers: member.canInviteMembers,
canRemoveMembers: member.canRemoveMembers,
canEditTeamSettings: member.canEditTeamSettings,
canViewTeamResources: member.canViewTeamResources,
canManageTeamResources: member.canManageTeamResources,
canCreateProjects: member.canCreateProjects,
canCreateServices: member.canCreateServices,
canDeleteProjects: member.canDeleteProjects,
canDeleteServices: member.canDeleteServices,
canAccessToTraefikFiles: member.canAccessToTraefikFiles,
canAccessToDocker: member.canAccessToDocker,
canAccessToAPI: member.canAccessToAPI,
canAccessToSSHKeys: member.canAccessToSSHKeys,
canAccessToGitProviders: member.canAccessToGitProviders,
accesedProjects: member.accesedProjects || [],
accesedServices: member.accesedServices || [],
});
}
}, [form, member]);

const onSubmit = async (data: TeamPermissions) => {
await mutateAsync({
teamId,
userId,
...data,
accesedServices: data.accesedServices || undefined
})
.then(async () => {
toast.success("Team member permissions updated");
refetch();
})
.catch(() => {
toast.error("Error updating team member permissions");
});
};

return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
Manage Permissions
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Team Member Permissions</DialogTitle>
<DialogDescription>
Configure permissions for this team member
</DialogDescription>
</DialogHeader>

{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}

<Form {...form}>
<form
id="hook-form-add-permissions"
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8"
>
<PermissionFormFields control={form.control} showTeamFields />

<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-add-permissions"
type="submit"
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
113 changes: 113 additions & 0 deletions apps/dokploy/components/dashboard/settings/teams/create-team.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { teamSchema } from "@dokploy/server/db/schema/team-schema";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { z } from "zod";

type FormData = z.infer<typeof teamSchema>;

export const CreateTeam = () => {
const utils = api.useContext();
const { mutateAsync, isLoading } = api.team.create.useMutation({
onSuccess: () => {
utils.team.all.invalidate();
},
});

const form = useForm<FormData>({
resolver: zodResolver(teamSchema),
defaultValues: {
name: "",
description: "",
},
});

const onSubmit = async (data: FormData) => {
try {
await mutateAsync(data);
toast.success("Team created successfully");
form.reset();
} catch (error) {
console.error("Create team error:", error);
toast.error(
error instanceof Error ? error.message : "Failed to create team",
);
}
};

return (
<Dialog>
<DialogTrigger asChild>
<Button>Create Team</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Team</DialogTitle>
<DialogDescription>
Create a new team and add members to it
</DialogDescription>
</DialogHeader>

<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Team Name</FormLabel>
<FormControl>
<Input placeholder="Enter team name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Enter team description"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<Button type="submit" isLoading={isLoading}>
Create Team
</Button>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { toast } from "sonner";

interface Props {
teamId: string;
}

export const DeleteTeam = ({ teamId }: Props) => {
const utils = api.useContext();
const { mutateAsync, isLoading } = api.team.delete.useMutation({
onSuccess: () => {
utils.team.all.invalidate();
},
});

const handleDelete = async () => {
try {
await mutateAsync({ teamId });
toast.success("Team deleted successfully");
} catch (error) {
toast.error("Failed to delete team");
}
};

return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="text-destructive"
>
Delete Team
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Team</DialogTitle>
<DialogDescription>
Are you sure you want to delete this team? This action cannot be
undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" type="button">
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
isLoading={isLoading}
>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
Loading
Loading