Skip to content

Commit 7224436

Browse files
authored
Merge pull request #4135 from Dokploy/feat/add-shared-git-providers
feat(git-provider): enhance sharing and permissions management
2 parents 86ba597 + d6885c3 commit 7224436

18 files changed

Lines changed: 8662 additions & 82 deletions

File tree

apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx

Lines changed: 123 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ImportIcon,
66
Loader2,
77
Trash2,
8+
Users,
89
} from "lucide-react";
910
import Link from "next/link";
1011
import { toast } from "sonner";
@@ -24,6 +25,13 @@ import {
2425
CardHeader,
2526
CardTitle,
2627
} from "@/components/ui/card";
28+
import { Switch } from "@/components/ui/switch";
29+
import {
30+
Tooltip,
31+
TooltipContent,
32+
TooltipProvider,
33+
TooltipTrigger,
34+
} from "@/components/ui/tooltip";
2735
import { api } from "@/utils/api";
2836
import { useUrl } from "@/utils/hooks/use-url";
2937
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
@@ -39,6 +47,8 @@ export const ShowGitProviders = () => {
3947
const { data, isPending, refetch } = api.gitProvider.getAll.useQuery();
4048
const { mutateAsync, isPending: isRemoving } =
4149
api.gitProvider.remove.useMutation();
50+
const { mutateAsync: toggleShare, isPending: isToggling } =
51+
api.gitProvider.toggleShare.useMutation();
4252
const url = useUrl();
4353

4454
const getGitlabUrl = (
@@ -154,10 +164,62 @@ export const ShowGitProviders = () => {
154164
)}
155165
</span>
156166
</div>
167+
{!gitProvider.isOwner && (
168+
<Badge
169+
variant="secondary"
170+
className="text-xs"
171+
>
172+
<Users className="size-3 mr-1" />
173+
Shared
174+
</Badge>
175+
)}
157176
</div>
158177
</div>
159178

160179
<div className="flex flex-row gap-1 items-center">
180+
{gitProvider.isOwner && (
181+
<TooltipProvider delayDuration={0}>
182+
<Tooltip>
183+
<TooltipTrigger asChild>
184+
<div className="flex items-center gap-1.5 mr-2">
185+
<Users className="size-4 text-muted-foreground" />
186+
<Switch
187+
disabled={isToggling}
188+
checked={
189+
gitProvider.sharedWithOrganization
190+
}
191+
onCheckedChange={async (
192+
checked,
193+
) => {
194+
await toggleShare({
195+
gitProviderId:
196+
gitProvider.gitProviderId,
197+
sharedWithOrganization: checked,
198+
})
199+
.then(() => {
200+
toast.success(
201+
checked
202+
? "Provider shared with organization"
203+
: "Provider unshared",
204+
);
205+
refetch();
206+
})
207+
.catch(() => {
208+
toast.error(
209+
"Error updating sharing",
210+
);
211+
});
212+
}}
213+
/>
214+
</div>
215+
</TooltipTrigger>
216+
<TooltipContent>
217+
Share with entire organization
218+
</TooltipContent>
219+
</Tooltip>
220+
</TooltipProvider>
221+
)}
222+
161223
{isBitbucket &&
162224
gitProvider.bitbucket?.appPassword &&
163225
!gitProvider.bitbucket?.apiToken ? (
@@ -222,62 +284,71 @@ export const ShowGitProviders = () => {
222284
</div>
223285
)}
224286

225-
{isGithub && haveGithubRequirements && (
226-
<EditGithubProvider
227-
githubId={gitProvider.github?.githubId}
228-
/>
229-
)}
287+
{gitProvider.isOwner && (
288+
<>
289+
{isGithub && haveGithubRequirements && (
290+
<EditGithubProvider
291+
githubId={gitProvider.github?.githubId}
292+
/>
293+
)}
230294

231-
{isGitlab && (
232-
<EditGitlabProvider
233-
gitlabId={gitProvider.gitlab?.gitlabId}
234-
/>
235-
)}
295+
{isGitlab && (
296+
<EditGitlabProvider
297+
gitlabId={gitProvider.gitlab?.gitlabId}
298+
/>
299+
)}
236300

237-
{isBitbucket && (
238-
<EditBitbucketProvider
239-
bitbucketId={
240-
gitProvider.bitbucket?.bitbucketId
241-
}
242-
/>
243-
)}
301+
{isBitbucket && (
302+
<EditBitbucketProvider
303+
bitbucketId={
304+
gitProvider.bitbucket?.bitbucketId
305+
}
306+
/>
307+
)}
244308

245-
{isGitea && (
246-
<EditGiteaProvider
247-
giteaId={gitProvider.gitea?.giteaId}
248-
/>
249-
)}
309+
{isGitea && (
310+
<EditGiteaProvider
311+
giteaId={gitProvider.gitea?.giteaId}
312+
/>
313+
)}
250314

251-
<DialogAction
252-
title="Delete Git Provider"
253-
description="Are you sure you want to delete this Git Provider?"
254-
type="destructive"
255-
onClick={async () => {
256-
await mutateAsync({
257-
gitProviderId: gitProvider.gitProviderId,
258-
})
259-
.then(() => {
260-
toast.success(
261-
"Git Provider deleted successfully",
262-
);
263-
refetch();
264-
})
265-
.catch(() => {
266-
toast.error(
267-
"Error deleting Git Provider",
268-
);
269-
});
270-
}}
271-
>
272-
<Button
273-
variant="ghost"
274-
size="icon"
275-
className="group hover:bg-red-500/10"
276-
isLoading={isRemoving}
277-
>
278-
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
279-
</Button>
280-
</DialogAction>
315+
<DialogAction
316+
title="Delete Git Provider"
317+
description={
318+
gitProvider.sharedWithOrganization
319+
? "This provider is shared with the organization. Deleting it will remove access for all members. Are you sure?"
320+
: "Are you sure you want to delete this Git Provider?"
321+
}
322+
type="destructive"
323+
onClick={async () => {
324+
await mutateAsync({
325+
gitProviderId:
326+
gitProvider.gitProviderId,
327+
})
328+
.then(() => {
329+
toast.success(
330+
"Git Provider deleted successfully",
331+
);
332+
refetch();
333+
})
334+
.catch(() => {
335+
toast.error(
336+
"Error deleting Git Provider",
337+
);
338+
});
339+
}}
340+
>
341+
<Button
342+
variant="ghost"
343+
size="icon"
344+
className="group hover:bg-red-500/10"
345+
isLoading={isRemoving}
346+
>
347+
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
348+
</Button>
349+
</DialogAction>
350+
</>
351+
)}
281352
</div>
282353
</div>
283354
</div>

apps/dokploy/components/dashboard/settings/users/add-permissions.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
FormMessage,
2727
} from "@/components/ui/form";
2828
import { Switch } from "@/components/ui/switch";
29+
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
2930
import { api, type RouterOutputs } from "@/utils/api";
3031

3132
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
@@ -170,6 +171,7 @@ const addPermissions = z.object({
170171
accessedProjects: z.array(z.string()).optional(),
171172
accessedEnvironments: z.array(z.string()).optional(),
172173
accessedServices: z.array(z.string()).optional(),
174+
accessedGitProviders: z.array(z.string()).optional(),
173175
canCreateProjects: z.boolean().optional().default(false),
174176
canCreateServices: z.boolean().optional().default(false),
175177
canDeleteProjects: z.boolean().optional().default(false),
@@ -196,6 +198,15 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
196198
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
197199
enabled: isOpen,
198200
});
201+
const { data: haveValidLicense } =
202+
api.licenseKey.haveValidLicenseKey.useQuery();
203+
204+
const { data: gitProviders } = api.gitProvider.allForPermissions.useQuery(
205+
undefined,
206+
{
207+
enabled: isOpen && !!haveValidLicense,
208+
},
209+
);
199210

200211
const { data, refetch } = api.user.one.useQuery(
201212
{
@@ -214,6 +225,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
214225
accessedProjects: [],
215226
accessedEnvironments: [],
216227
accessedServices: [],
228+
accessedGitProviders: [],
217229
canDeleteEnvironments: false,
218230
canCreateProjects: false,
219231
canCreateServices: false,
@@ -235,6 +247,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
235247
accessedProjects: data.accessedProjects || [],
236248
accessedEnvironments: data.accessedEnvironments || [],
237249
accessedServices: data.accessedServices || [],
250+
accessedGitProviders: data.accessedGitProviders || [],
238251
canCreateProjects: data.canCreateProjects,
239252
canCreateServices: data.canCreateServices,
240253
canDeleteProjects: data.canDeleteProjects,
@@ -262,6 +275,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
262275
accessedProjects: data.accessedProjects || [],
263276
accessedEnvironments: data.accessedEnvironments || [],
264277
accessedServices: data.accessedServices || [],
278+
accessedGitProviders: data.accessedGitProviders || [],
265279
canAccessToDocker: data.canAccessToDocker,
266280
canAccessToAPI: data.canAccessToAPI,
267281
canAccessToSSHKeys: data.canAccessToSSHKeys,
@@ -870,6 +884,78 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
870884
</FormItem>
871885
)}
872886
/>
887+
{haveValidLicense ? (
888+
<FormField
889+
control={form.control}
890+
name="accessedGitProviders"
891+
render={() => (
892+
<FormItem className="md:col-span-2">
893+
<div className="mb-4">
894+
<FormLabel className="text-base">Git Providers</FormLabel>
895+
<FormDescription>
896+
Select the Git Providers that the user can access
897+
</FormDescription>
898+
</div>
899+
{gitProviders?.length === 0 && (
900+
<p className="text-sm text-muted-foreground">
901+
No git providers found
902+
</p>
903+
)}
904+
<div className="grid md:grid-cols-1 gap-2">
905+
{gitProviders?.map((provider) => (
906+
<FormField
907+
key={provider.gitProviderId}
908+
control={form.control}
909+
name="accessedGitProviders"
910+
render={({ field }) => (
911+
<FormItem className="flex flex-row items-center space-x-3 space-y-0 rounded-lg border p-3">
912+
<FormControl>
913+
<Checkbox
914+
checked={field.value?.includes(
915+
provider.gitProviderId,
916+
)}
917+
onCheckedChange={(checked) => {
918+
if (checked) {
919+
field.onChange([
920+
...(field.value || []),
921+
provider.gitProviderId,
922+
]);
923+
} else {
924+
field.onChange(
925+
field.value?.filter(
926+
(v) => v !== provider.gitProviderId,
927+
),
928+
);
929+
}
930+
}}
931+
/>
932+
</FormControl>
933+
<div className="flex items-center gap-2">
934+
<FormLabel className="text-sm cursor-pointer">
935+
{provider.name}
936+
</FormLabel>
937+
<span className="text-xs text-muted-foreground capitalize">
938+
({provider.providerType})
939+
</span>
940+
</div>
941+
</FormItem>
942+
)}
943+
/>
944+
))}
945+
</div>
946+
<FormMessage />
947+
</FormItem>
948+
)}
949+
/>
950+
) : (
951+
<div className="md:col-span-2">
952+
<EnterpriseFeatureLocked
953+
compact
954+
title="Git Provider Assignment"
955+
description="Assign specific Git Providers to users with an Enterprise license."
956+
/>
957+
</div>
958+
)}
873959
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2">
874960
<Button
875961
isLoading={isPending}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE "member" ADD COLUMN "accessedGitProviders" text[] DEFAULT ARRAY[]::text[] NOT NULL;--> statement-breakpoint
2+
ALTER TABLE "git_provider" ADD COLUMN "sharedWithOrganization" boolean DEFAULT false NOT NULL;

0 commit comments

Comments
 (0)