mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-13 19:09:49 +00:00
8127dc4536
- Introduced new test files for permission checks, including `check-permission.test.ts`, `enterprise-only-resources.test.ts`, `resolve-permissions.test.ts`, and `service-access.test.ts`. - Implemented permission checks in various components to ensure actions are gated by user permissions, including `ShowTraefikConfig`, `UpdateTraefikConfig`, `ShowVolumes`, `ShowDomains`, and others. - Enhanced the logic for displaying UI elements based on user permissions, ensuring that only authorized users can access or modify resources.
874 lines
29 KiB
TypeScript
874 lines
29 KiB
TypeScript
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
|
import { useEffect, useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { toast } from "sonner";
|
|
import { z } from "zod";
|
|
import { AlertBlock } from "@/components/shared/alert-block";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { api, type RouterOutputs } from "@/utils/api";
|
|
|
|
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
|
|
type ProjectForPermissions =
|
|
RouterOutputs["project"]["allForPermissions"][number];
|
|
type EnvironmentForPermissions = ProjectForPermissions["environments"][number];
|
|
|
|
type Environment = EnvironmentForPermissions;
|
|
|
|
export type Services = {
|
|
appName: string;
|
|
serverId?: string | null;
|
|
name: string;
|
|
type:
|
|
| "mariadb"
|
|
| "application"
|
|
| "postgres"
|
|
| "mysql"
|
|
| "mongo"
|
|
| "redis"
|
|
| "compose";
|
|
description?: string | null;
|
|
id: string;
|
|
createdAt: string;
|
|
status?: "idle" | "running" | "done" | "error";
|
|
};
|
|
|
|
export const extractServices = (data: Environment | undefined) => {
|
|
const applications: Services[] = (data?.applications?.map((item) => ({
|
|
appName: item.appName,
|
|
name: item.name,
|
|
type: "application",
|
|
id: item.applicationId,
|
|
createdAt: item.createdAt,
|
|
status: item.applicationStatus,
|
|
description: item.description,
|
|
serverId: item.serverId,
|
|
})) ?? []) as Services[];
|
|
|
|
const mariadb: Services[] =
|
|
data?.mariadb.map((item) => ({
|
|
appName: item.appName,
|
|
name: item.name,
|
|
type: "mariadb",
|
|
id: item.mariadbId,
|
|
createdAt: item.createdAt,
|
|
status: item.applicationStatus,
|
|
description: item.description,
|
|
serverId: item.serverId,
|
|
})) || [];
|
|
|
|
const postgres: Services[] =
|
|
data?.postgres.map((item) => ({
|
|
appName: item.appName,
|
|
name: item.name,
|
|
type: "postgres",
|
|
id: item.postgresId,
|
|
createdAt: item.createdAt,
|
|
status: item.applicationStatus,
|
|
description: item.description,
|
|
serverId: item.serverId,
|
|
})) || [];
|
|
|
|
const mongo: Services[] =
|
|
data?.mongo.map((item) => ({
|
|
appName: item.appName,
|
|
name: item.name,
|
|
type: "mongo",
|
|
id: item.mongoId,
|
|
createdAt: item.createdAt,
|
|
status: item.applicationStatus,
|
|
description: item.description,
|
|
serverId: item.serverId,
|
|
})) || [];
|
|
|
|
const redis: Services[] =
|
|
data?.redis.map((item) => ({
|
|
appName: item.appName,
|
|
name: item.name,
|
|
type: "redis",
|
|
id: item.redisId,
|
|
createdAt: item.createdAt,
|
|
status: item.applicationStatus,
|
|
description: item.description,
|
|
serverId: item.serverId,
|
|
})) || [];
|
|
|
|
const mysql: Services[] =
|
|
data?.mysql.map((item) => ({
|
|
appName: item.appName,
|
|
name: item.name,
|
|
type: "mysql",
|
|
id: item.mysqlId,
|
|
createdAt: item.createdAt,
|
|
status: item.applicationStatus,
|
|
description: item.description,
|
|
serverId: item.serverId,
|
|
})) || [];
|
|
|
|
const compose: Services[] = (data?.compose?.map((item) => ({
|
|
appName: item.appName,
|
|
name: item.name,
|
|
type: "compose",
|
|
id: item.composeId,
|
|
createdAt: item.createdAt,
|
|
status: item.composeStatus,
|
|
description: item.description,
|
|
serverId: item.serverId,
|
|
})) ?? []) as Services[];
|
|
|
|
applications.push(
|
|
...mysql,
|
|
...redis,
|
|
...mongo,
|
|
...postgres,
|
|
...mariadb,
|
|
...compose,
|
|
);
|
|
|
|
applications.sort((a, b) => {
|
|
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
});
|
|
|
|
return applications;
|
|
};
|
|
|
|
const addPermissions = z.object({
|
|
accessedProjects: z.array(z.string()).optional(),
|
|
accessedEnvironments: z.array(z.string()).optional(),
|
|
accessedServices: z.array(z.string()).optional(),
|
|
canCreateProjects: z.boolean().optional().default(false),
|
|
canCreateServices: z.boolean().optional().default(false),
|
|
canDeleteProjects: z.boolean().optional().default(false),
|
|
canDeleteServices: z.boolean().optional().default(false),
|
|
canDeleteEnvironments: z.boolean().optional().default(false),
|
|
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
|
canAccessToDocker: z.boolean().optional().default(false),
|
|
canAccessToAPI: z.boolean().optional().default(false),
|
|
canAccessToSSHKeys: z.boolean().optional().default(false),
|
|
canAccessToGitProviders: z.boolean().optional().default(false),
|
|
canCreateEnvironments: z.boolean().optional().default(false),
|
|
});
|
|
|
|
type AddPermissions = z.infer<typeof addPermissions>;
|
|
|
|
interface Props {
|
|
userId: string;
|
|
role?: string;
|
|
}
|
|
|
|
export const AddUserPermissions = ({ userId, role }: Props) => {
|
|
const isCustomRole = !!role && !["owner", "admin", "member"].includes(role);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
|
enabled: isOpen,
|
|
});
|
|
|
|
const { data, refetch } = api.user.one.useQuery(
|
|
{
|
|
userId,
|
|
},
|
|
{
|
|
enabled: !!userId,
|
|
},
|
|
);
|
|
|
|
const { mutateAsync, isError, error, isPending } =
|
|
api.user.assignPermissions.useMutation();
|
|
|
|
const form = useForm({
|
|
defaultValues: {
|
|
accessedProjects: [],
|
|
accessedEnvironments: [],
|
|
accessedServices: [],
|
|
canDeleteEnvironments: false,
|
|
canCreateProjects: false,
|
|
canCreateServices: false,
|
|
canDeleteProjects: false,
|
|
canDeleteServices: false,
|
|
canAccessToTraefikFiles: false,
|
|
canAccessToDocker: false,
|
|
canAccessToAPI: false,
|
|
canAccessToSSHKeys: false,
|
|
canAccessToGitProviders: false,
|
|
canCreateEnvironments: false,
|
|
},
|
|
resolver: zodResolver(addPermissions),
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (data && isOpen) {
|
|
form.reset({
|
|
accessedProjects: data.accessedProjects || [],
|
|
accessedEnvironments: data.accessedEnvironments || [],
|
|
accessedServices: data.accessedServices || [],
|
|
canCreateProjects: data.canCreateProjects,
|
|
canCreateServices: data.canCreateServices,
|
|
canDeleteProjects: data.canDeleteProjects,
|
|
canDeleteServices: data.canDeleteServices,
|
|
canDeleteEnvironments: data.canDeleteEnvironments || false,
|
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
|
canAccessToDocker: data.canAccessToDocker,
|
|
canAccessToAPI: data.canAccessToAPI,
|
|
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
|
canAccessToGitProviders: data.canAccessToGitProviders,
|
|
canCreateEnvironments: data.canCreateEnvironments,
|
|
});
|
|
}
|
|
}, [form, form.reset, data, isOpen]);
|
|
|
|
const onSubmit = async (data: AddPermissions) => {
|
|
await mutateAsync({
|
|
id: userId,
|
|
canCreateServices: data.canCreateServices,
|
|
canCreateProjects: data.canCreateProjects,
|
|
canDeleteServices: data.canDeleteServices,
|
|
canDeleteProjects: data.canDeleteProjects,
|
|
canDeleteEnvironments: data.canDeleteEnvironments,
|
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
|
accessedProjects: data.accessedProjects || [],
|
|
accessedEnvironments: data.accessedEnvironments || [],
|
|
accessedServices: data.accessedServices || [],
|
|
canAccessToDocker: data.canAccessToDocker,
|
|
canAccessToAPI: data.canAccessToAPI,
|
|
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
|
canAccessToGitProviders: data.canAccessToGitProviders,
|
|
canCreateEnvironments: data.canCreateEnvironments,
|
|
})
|
|
.then(async () => {
|
|
toast.success("Permissions updated");
|
|
refetch();
|
|
setIsOpen(false);
|
|
})
|
|
.catch(() => {
|
|
toast.error("Error updating the permissions");
|
|
});
|
|
};
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<DialogTrigger className="" asChild>
|
|
<DropdownMenuItem
|
|
className="w-full cursor-pointer"
|
|
onSelect={(e) => e.preventDefault()}
|
|
>
|
|
Add Permissions
|
|
</DropdownMenuItem>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-h-[85vh] sm:max-w-4xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Permissions</DialogTitle>
|
|
<DialogDescription>Add or remove permissions</DialogDescription>
|
|
</DialogHeader>
|
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
|
|
<Form {...form}>
|
|
<form
|
|
id="hook-form-add-permissions"
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
|
>
|
|
{isCustomRole && (
|
|
<div className="md:col-span-2 rounded-lg border p-3 bg-muted/50 text-sm text-muted-foreground">
|
|
This user has a custom role assigned. Capabilities are defined
|
|
by the role. You can still manage which projects, environments,
|
|
and services they can access below.
|
|
</div>
|
|
)}
|
|
{!isCustomRole && (
|
|
<>
|
|
<FormField
|
|
control={form.control}
|
|
name="canCreateProjects"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Create Projects</FormLabel>
|
|
<FormDescription>
|
|
Allow the user to create projects
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="canDeleteProjects"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Delete Projects</FormLabel>
|
|
<FormDescription>
|
|
Allow the user to delete projects
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="canCreateServices"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Create Services</FormLabel>
|
|
<FormDescription>
|
|
Allow the user to create services
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="canDeleteServices"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Delete Services</FormLabel>
|
|
<FormDescription>
|
|
Allow the user to delete services
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="canCreateEnvironments"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Create Environments</FormLabel>
|
|
<FormDescription>
|
|
Allow the user to create environments
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="canDeleteEnvironments"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Delete Environments</FormLabel>
|
|
<FormDescription>
|
|
Allow the user to delete environments
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="canAccessToTraefikFiles"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Access to Traefik Files</FormLabel>
|
|
<FormDescription>
|
|
Allow the user to access to the Traefik Tab Files
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="canAccessToDocker"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Access to Docker</FormLabel>
|
|
<FormDescription>
|
|
Allow the user to access to the Docker Tab
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="canAccessToAPI"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Access to API/CLI</FormLabel>
|
|
<FormDescription>
|
|
Allow the user to access to the API/CLI
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="canAccessToSSHKeys"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Access to SSH Keys</FormLabel>
|
|
<FormDescription>
|
|
Allow to users to access to the SSH Keys section
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="canAccessToGitProviders"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
<div className="space-y-0.5">
|
|
<FormLabel>Access to Git Providers</FormLabel>
|
|
<FormDescription>
|
|
Allow to users to access to the Git Providers section
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</>
|
|
)}
|
|
<FormField
|
|
control={form.control}
|
|
name="accessedProjects"
|
|
render={() => (
|
|
<FormItem className="md:col-span-2">
|
|
<div className="mb-4">
|
|
<FormLabel className="text-base">Projects</FormLabel>
|
|
<FormDescription>
|
|
Select the Projects that the user can access
|
|
</FormDescription>
|
|
</div>
|
|
{projects?.length === 0 && (
|
|
<p className="text-sm text-muted-foreground">
|
|
No projects found
|
|
</p>
|
|
)}
|
|
<div className="grid md:grid-cols-1 gap-4">
|
|
{projects?.map((project, projectIndex) => {
|
|
return (
|
|
<FormField
|
|
key={`project-${projectIndex}`}
|
|
control={form.control}
|
|
name="accessedProjects"
|
|
render={({ field }) => {
|
|
return (
|
|
<FormItem
|
|
key={project.projectId}
|
|
className="flex flex-col items-start rounded-lg p-4 border"
|
|
>
|
|
{/* Project Header */}
|
|
<div className="flex flex-row gap-4 items-center w-full">
|
|
<FormControl>
|
|
<Checkbox
|
|
checked={field.value?.includes(
|
|
project.projectId,
|
|
)}
|
|
onCheckedChange={(checked) => {
|
|
if (checked) {
|
|
// Add the project
|
|
field.onChange([
|
|
...(field.value || []),
|
|
project.projectId,
|
|
]);
|
|
} else {
|
|
// Remove the project
|
|
field.onChange(
|
|
field.value?.filter(
|
|
(value) =>
|
|
value !== project.projectId,
|
|
),
|
|
);
|
|
|
|
// Also remove all environments and services from this project
|
|
const currentEnvs =
|
|
form.getValues(
|
|
"accessedEnvironments",
|
|
) || [];
|
|
const currentServices =
|
|
form.getValues(
|
|
"accessedServices",
|
|
) || [];
|
|
|
|
// Get all environment IDs from this project
|
|
const projectEnvIds =
|
|
project.environments.map(
|
|
(env) => env.environmentId,
|
|
);
|
|
|
|
// Get all service IDs from this project
|
|
const projectServiceIds =
|
|
project.environments.flatMap(
|
|
(env) =>
|
|
extractServices(env).map(
|
|
(service) => service.id,
|
|
),
|
|
);
|
|
|
|
// Remove environments and services from this project
|
|
form.setValue(
|
|
"accessedEnvironments",
|
|
currentEnvs.filter(
|
|
(envId) =>
|
|
!projectEnvIds.includes(envId),
|
|
),
|
|
);
|
|
form.setValue(
|
|
"accessedServices",
|
|
currentServices.filter(
|
|
(serviceId) =>
|
|
!projectServiceIds.includes(
|
|
serviceId,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<FormLabel className="text-base font-semibold text-primary">
|
|
{project.name}
|
|
</FormLabel>
|
|
</div>
|
|
|
|
{/* Environments */}
|
|
<div className="ml-6 w-full space-y-3">
|
|
{project.environments.length === 0 && (
|
|
<p className="text-sm text-muted-foreground">
|
|
No environments found
|
|
</p>
|
|
)}
|
|
{project.environments.map(
|
|
(environment, envIndex) => {
|
|
const services =
|
|
extractServices(environment);
|
|
return (
|
|
<div
|
|
key={`env-${envIndex}`}
|
|
className="border-l-2 border-muted pl-4"
|
|
>
|
|
{/* Environment Header with Checkbox */}
|
|
<FormField
|
|
key={`env-${envIndex}`}
|
|
control={form.control}
|
|
name="accessedEnvironments"
|
|
render={({ field: envField }) => (
|
|
<FormItem className="flex flex-row items-center space-x-3 space-y-0 mb-2">
|
|
<FormControl>
|
|
<Checkbox
|
|
checked={envField.value?.includes(
|
|
environment.environmentId,
|
|
)}
|
|
onCheckedChange={(
|
|
checked,
|
|
) => {
|
|
if (checked) {
|
|
// Add the environment
|
|
envField.onChange([
|
|
...(envField.value ||
|
|
[]),
|
|
environment.environmentId,
|
|
]);
|
|
|
|
// Auto-select the project if not already selected
|
|
const currentProjects =
|
|
form.getValues(
|
|
"accessedProjects",
|
|
) || [];
|
|
if (
|
|
!currentProjects.includes(
|
|
project.projectId,
|
|
)
|
|
) {
|
|
form.setValue(
|
|
"accessedProjects",
|
|
[
|
|
...currentProjects,
|
|
project.projectId,
|
|
],
|
|
);
|
|
}
|
|
} else {
|
|
// Remove the environment
|
|
envField.onChange(
|
|
envField.value?.filter(
|
|
(value) =>
|
|
value !==
|
|
environment.environmentId,
|
|
),
|
|
);
|
|
|
|
// Also remove all services from this environment
|
|
const currentServices =
|
|
form.getValues(
|
|
"accessedServices",
|
|
) || [];
|
|
const environmentServiceIds =
|
|
services.map(
|
|
(service) =>
|
|
service.id,
|
|
);
|
|
|
|
form.setValue(
|
|
"accessedServices",
|
|
currentServices.filter(
|
|
(serviceId) =>
|
|
!environmentServiceIds.includes(
|
|
serviceId,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full" />
|
|
<FormLabel className="text-sm font-medium text-foreground cursor-pointer">
|
|
{environment.name}
|
|
</FormLabel>
|
|
<span className="text-xs text-muted-foreground">
|
|
({services.length} services)
|
|
</span>
|
|
</div>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{/* Services */}
|
|
<div className="ml-4 space-y-2">
|
|
{services.length === 0 && (
|
|
<p className="text-xs text-muted-foreground">
|
|
No services found
|
|
</p>
|
|
)}
|
|
{services.map(
|
|
(service, serviceIndex) => (
|
|
<FormField
|
|
key={`service-${serviceIndex}`}
|
|
control={form.control}
|
|
name="accessedServices"
|
|
render={({
|
|
field: serviceField,
|
|
}) => {
|
|
return (
|
|
<FormItem
|
|
key={service.id}
|
|
className="flex flex-row items-center space-x-3 space-y-0"
|
|
>
|
|
<FormControl>
|
|
<Checkbox
|
|
checked={serviceField.value?.includes(
|
|
service.id,
|
|
)}
|
|
onCheckedChange={(
|
|
checked,
|
|
) => {
|
|
if (checked) {
|
|
// Add the service
|
|
serviceField.onChange(
|
|
[
|
|
...(serviceField.value ||
|
|
[]),
|
|
service.id,
|
|
],
|
|
);
|
|
|
|
// Auto-select the environment if not already selected
|
|
const currentEnvs =
|
|
form.getValues(
|
|
"accessedEnvironments",
|
|
) || [];
|
|
if (
|
|
!currentEnvs.includes(
|
|
environment.environmentId,
|
|
)
|
|
) {
|
|
form.setValue(
|
|
"accessedEnvironments",
|
|
[
|
|
...currentEnvs,
|
|
environment.environmentId,
|
|
],
|
|
);
|
|
}
|
|
|
|
// Auto-select the project if not already selected
|
|
const currentProjects =
|
|
form.getValues(
|
|
"accessedProjects",
|
|
) || [];
|
|
if (
|
|
!currentProjects.includes(
|
|
project.projectId,
|
|
)
|
|
) {
|
|
form.setValue(
|
|
"accessedProjects",
|
|
[
|
|
...currentProjects,
|
|
project.projectId,
|
|
],
|
|
);
|
|
}
|
|
} else {
|
|
// Remove the service
|
|
serviceField.onChange(
|
|
serviceField.value?.filter(
|
|
(value) =>
|
|
value !==
|
|
service.id,
|
|
),
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`w-1.5 h-1.5 rounded-full ${
|
|
service.type ===
|
|
"application"
|
|
? "bg-green-500"
|
|
: service.type ===
|
|
"compose"
|
|
? "bg-purple-500"
|
|
: "bg-orange-500"
|
|
}`}
|
|
/>
|
|
<FormLabel className="text-sm text-muted-foreground cursor-pointer">
|
|
{service.name}
|
|
</FormLabel>
|
|
<span className="text-xs text-muted-foreground/70 capitalize">
|
|
({service.type})
|
|
</span>
|
|
</div>
|
|
</FormItem>
|
|
);
|
|
}}
|
|
/>
|
|
),
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
)}
|
|
</div>
|
|
</FormItem>
|
|
);
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2">
|
|
<Button
|
|
isLoading={isPending}
|
|
form="hook-form-add-permissions"
|
|
type="submit"
|
|
>
|
|
Update
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|