nobalmako 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -0
- package/components.json +22 -0
- package/dist/nobalmako.js +272 -0
- package/drizzle/0000_pink_spiral.sql +126 -0
- package/drizzle/meta/0000_snapshot.json +1027 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +7 -0
- package/package.json +80 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/index.ts +118 -0
- package/src/app/api/api-keys/[id]/route.ts +147 -0
- package/src/app/api/api-keys/route.ts +151 -0
- package/src/app/api/audit-logs/route.ts +84 -0
- package/src/app/api/auth/forgot-password/route.ts +47 -0
- package/src/app/api/auth/login/route.ts +99 -0
- package/src/app/api/auth/logout/route.ts +15 -0
- package/src/app/api/auth/me/route.ts +23 -0
- package/src/app/api/auth/mfa/setup/route.ts +33 -0
- package/src/app/api/auth/mfa/verify/route.ts +45 -0
- package/src/app/api/auth/register/route.ts +140 -0
- package/src/app/api/auth/reset-password/route.ts +52 -0
- package/src/app/api/auth/update/route.ts +71 -0
- package/src/app/api/auth/verify/route.ts +39 -0
- package/src/app/api/environments/route.ts +227 -0
- package/src/app/api/team-members/route.ts +385 -0
- package/src/app/api/teams/route.ts +217 -0
- package/src/app/api/variable-history/route.ts +218 -0
- package/src/app/api/variables/route.ts +476 -0
- package/src/app/api/webhooks/route.ts +77 -0
- package/src/app/api-keys/APIKeysClient.tsx +316 -0
- package/src/app/api-keys/page.tsx +10 -0
- package/src/app/api-reference/page.tsx +324 -0
- package/src/app/audit-log/AuditLogClient.tsx +229 -0
- package/src/app/audit-log/page.tsx +10 -0
- package/src/app/auth/forgot-password/page.tsx +121 -0
- package/src/app/auth/login/LoginForm.tsx +145 -0
- package/src/app/auth/login/page.tsx +11 -0
- package/src/app/auth/register/RegisterForm.tsx +156 -0
- package/src/app/auth/register/page.tsx +16 -0
- package/src/app/auth/reset-password/page.tsx +160 -0
- package/src/app/dashboard/DashboardClient.tsx +219 -0
- package/src/app/dashboard/page.tsx +11 -0
- package/src/app/docs/page.tsx +251 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +231 -0
- package/src/app/profile/ProfileClient.tsx +230 -0
- package/src/app/profile/page.tsx +10 -0
- package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
- package/src/app/project/[id]/page.tsx +17 -0
- package/src/bin/nobalmako.ts +341 -0
- package/src/components/ApiKeysManager.tsx +529 -0
- package/src/components/AppLayout.tsx +193 -0
- package/src/components/BulkActions.tsx +138 -0
- package/src/components/CreateEnvironmentDialog.tsx +207 -0
- package/src/components/CreateTeamDialog.tsx +174 -0
- package/src/components/CreateVariableDialog.tsx +311 -0
- package/src/components/DeleteEnvironmentDialog.tsx +104 -0
- package/src/components/DeleteTeamDialog.tsx +112 -0
- package/src/components/DeleteVariableDialog.tsx +103 -0
- package/src/components/EditEnvironmentDialog.tsx +202 -0
- package/src/components/EditMemberDialog.tsx +143 -0
- package/src/components/EditTeamDialog.tsx +178 -0
- package/src/components/EditVariableDialog.tsx +231 -0
- package/src/components/ImportVariablesDialog.tsx +347 -0
- package/src/components/InviteMemberDialog.tsx +191 -0
- package/src/components/LeaveProjectDialog.tsx +111 -0
- package/src/components/MFASettings.tsx +136 -0
- package/src/components/ProjectDiff.tsx +123 -0
- package/src/components/Providers.tsx +24 -0
- package/src/components/RemoveMemberDialog.tsx +112 -0
- package/src/components/SearchDialog.tsx +276 -0
- package/src/components/SecurityOverview.tsx +92 -0
- package/src/components/TeamMembersManager.tsx +103 -0
- package/src/components/VariableHistoryDialog.tsx +265 -0
- package/src/components/WebhooksManager.tsx +169 -0
- package/src/components/ui/alert-dialog.tsx +160 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/hooks/use-api-keys.ts +95 -0
- package/src/hooks/use-audit-logs.ts +58 -0
- package/src/hooks/use-auth.tsx +121 -0
- package/src/hooks/use-environments.ts +33 -0
- package/src/hooks/use-project-permissions.ts +49 -0
- package/src/hooks/use-team-members.ts +30 -0
- package/src/hooks/use-teams.ts +33 -0
- package/src/hooks/use-variables.ts +38 -0
- package/src/lib/audit.ts +36 -0
- package/src/lib/auth.ts +108 -0
- package/src/lib/crypto.ts +39 -0
- package/src/lib/db.ts +15 -0
- package/src/lib/dynamic-providers.ts +19 -0
- package/src/lib/email.ts +110 -0
- package/src/lib/mail.ts +51 -0
- package/src/lib/permissions.ts +51 -0
- package/src/lib/schema.ts +240 -0
- package/src/lib/seed.ts +107 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/webhooks.ts +42 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import {
|
|
7
|
+
AlertDialog,
|
|
8
|
+
AlertDialogAction,
|
|
9
|
+
AlertDialogCancel,
|
|
10
|
+
AlertDialogContent,
|
|
11
|
+
AlertDialogDescription,
|
|
12
|
+
AlertDialogFooter,
|
|
13
|
+
AlertDialogHeader,
|
|
14
|
+
AlertDialogTitle,
|
|
15
|
+
AlertDialogTrigger,
|
|
16
|
+
} from "@/components/ui/alert-dialog";
|
|
17
|
+
import { Trash2, AlertTriangle, ShieldAlert, Skull } from "lucide-react";
|
|
18
|
+
import { useTeams } from "@/hooks/use-teams";
|
|
19
|
+
|
|
20
|
+
interface DeleteTeamDialogProps {
|
|
21
|
+
teamId: string;
|
|
22
|
+
teamName: string;
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function DeleteTeamDialog({ teamId, teamName, children }: DeleteTeamDialogProps) {
|
|
27
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
28
|
+
const [error, setError] = useState("");
|
|
29
|
+
const [open, setOpen] = useState(false);
|
|
30
|
+
const queryClient = useQueryClient();
|
|
31
|
+
|
|
32
|
+
const handleDelete = async (e: React.MouseEvent) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setError("");
|
|
35
|
+
setIsLoading(true);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch('/api/teams', {
|
|
39
|
+
method: 'DELETE',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({ id: teamId }),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
setOpen(false);
|
|
50
|
+
queryClient.invalidateQueries({ queryKey: ['teams'] });
|
|
51
|
+
} else {
|
|
52
|
+
setError(data.error || "Failed to delete team");
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
setError("An unexpected error occurred");
|
|
56
|
+
} finally {
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
63
|
+
<AlertDialogTrigger asChild>
|
|
64
|
+
{children || (
|
|
65
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 hover:text-destructive hover:bg-destructive/10 transition-colors">
|
|
66
|
+
<Trash2 className="h-4 w-4" />
|
|
67
|
+
</Button>
|
|
68
|
+
)}
|
|
69
|
+
</AlertDialogTrigger>
|
|
70
|
+
<AlertDialogContent className="sm:max-w-[450px]">
|
|
71
|
+
<AlertDialogHeader>
|
|
72
|
+
<div className="flex items-center gap-3 mb-2">
|
|
73
|
+
<div className="p-2 bg-destructive/10 rounded-full text-destructive">
|
|
74
|
+
<Skull className="h-5 w-5" />
|
|
75
|
+
</div>
|
|
76
|
+
<AlertDialogTitle className="text-xl">Delete Project Permanently</AlertDialogTitle>
|
|
77
|
+
</div>
|
|
78
|
+
<AlertDialogDescription className="text-base text-muted-foreground pt-1">
|
|
79
|
+
Are you sure you want to delete <span className="font-bold text-foreground bg-muted px-2 rounded">"{teamName}"</span>?
|
|
80
|
+
<div className="mt-4 p-4 bg-destructive/5 rounded-lg border border-destructive/20 text-sm text-destructive font-medium space-y-3">
|
|
81
|
+
<div className="flex gap-2">
|
|
82
|
+
<AlertTriangle className="h-5 w-5 shrink-0" />
|
|
83
|
+
<p>CRITICAL: This will immediately and permanently destroy:</p>
|
|
84
|
+
</div>
|
|
85
|
+
<ul className="list-disc list-inside space-y-1 pl-7 opacity-90 font-normal">
|
|
86
|
+
<li>All project configurations and metadata</li>
|
|
87
|
+
<li>Every environment (Prod, Staging, etc.)</li>
|
|
88
|
+
<li>ALL encrypted secrets and variables</li>
|
|
89
|
+
<li>Project audit logs and activity history</li>
|
|
90
|
+
</ul>
|
|
91
|
+
</div>
|
|
92
|
+
</AlertDialogDescription>
|
|
93
|
+
</AlertDialogHeader>
|
|
94
|
+
{error && (
|
|
95
|
+
<div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20 my-2">
|
|
96
|
+
{error}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
<AlertDialogFooter className="pt-6 gap-2">
|
|
100
|
+
<AlertDialogCancel disabled={isLoading} className="mt-0">Cancel</AlertDialogCancel>
|
|
101
|
+
<AlertDialogAction
|
|
102
|
+
onClick={handleDelete}
|
|
103
|
+
disabled={isLoading}
|
|
104
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 px-8 shadow-sm shadow-destructive/20 border-0"
|
|
105
|
+
>
|
|
106
|
+
{isLoading ? "Deleting..." : "Delete Full Project"}
|
|
107
|
+
</AlertDialogAction>
|
|
108
|
+
</AlertDialogFooter>
|
|
109
|
+
</AlertDialogContent>
|
|
110
|
+
</AlertDialog>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import {
|
|
6
|
+
AlertDialog,
|
|
7
|
+
AlertDialogAction,
|
|
8
|
+
AlertDialogCancel,
|
|
9
|
+
AlertDialogContent,
|
|
10
|
+
AlertDialogDescription,
|
|
11
|
+
AlertDialogFooter,
|
|
12
|
+
AlertDialogHeader,
|
|
13
|
+
AlertDialogTitle,
|
|
14
|
+
AlertDialogTrigger,
|
|
15
|
+
} from "@/components/ui/alert-dialog";
|
|
16
|
+
import { Trash2, AlertTriangle } from "lucide-react";
|
|
17
|
+
import { useVariables } from "@/hooks/use-variables";
|
|
18
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
19
|
+
|
|
20
|
+
interface DeleteVariableDialogProps {
|
|
21
|
+
variableId: string;
|
|
22
|
+
variableKey: string;
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function DeleteVariableDialog({ variableId, variableKey, children }: DeleteVariableDialogProps) {
|
|
27
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
28
|
+
const [error, setError] = useState("");
|
|
29
|
+
const [open, setOpen] = useState(false);
|
|
30
|
+
const queryClient = useQueryClient();
|
|
31
|
+
|
|
32
|
+
const handleDelete = async (e: React.MouseEvent) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setError("");
|
|
35
|
+
setIsLoading(true);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch('/api/variables', {
|
|
39
|
+
method: 'DELETE',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({ id: variableId }),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
setOpen(false);
|
|
50
|
+
queryClient.invalidateQueries({ queryKey: ["variables"] });
|
|
51
|
+
} else {
|
|
52
|
+
setError(data.error || "Failed to delete variable");
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
setError("An unexpected error occurred");
|
|
56
|
+
} finally {
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
63
|
+
<AlertDialogTrigger asChild>
|
|
64
|
+
{children || (
|
|
65
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 hover:text-destructive hover:bg-destructive/10 transition-colors">
|
|
66
|
+
<Trash2 className="h-4 w-4" />
|
|
67
|
+
</Button>
|
|
68
|
+
)}
|
|
69
|
+
</AlertDialogTrigger>
|
|
70
|
+
<AlertDialogContent className="sm:max-w-[425px]">
|
|
71
|
+
<AlertDialogHeader>
|
|
72
|
+
<div className="flex items-center gap-3 mb-2">
|
|
73
|
+
<div className="p-2 bg-destructive/10 rounded-full">
|
|
74
|
+
<AlertTriangle className="h-5 w-5 text-destructive" />
|
|
75
|
+
</div>
|
|
76
|
+
<AlertDialogTitle className="text-xl">Delete Variable</AlertDialogTitle>
|
|
77
|
+
</div>
|
|
78
|
+
<AlertDialogDescription className="text-base text-muted-foreground pt-1">
|
|
79
|
+
Are you sure you want to delete the variable <span className="font-mono font-bold text-foreground bg-muted px-1.5 rounded">"{variableKey}"</span>?
|
|
80
|
+
<div className="mt-3 p-3 bg-muted/50 rounded-lg border border-muted text-sm italic">
|
|
81
|
+
All historical values and logs for this variable will be permanently removed.
|
|
82
|
+
</div>
|
|
83
|
+
</AlertDialogDescription>
|
|
84
|
+
</AlertDialogHeader>
|
|
85
|
+
{error && (
|
|
86
|
+
<div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20 my-2">
|
|
87
|
+
{error}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
<AlertDialogFooter className="pt-4 gap-2">
|
|
91
|
+
<AlertDialogCancel disabled={isLoading} className="mt-0">Cancel</AlertDialogCancel>
|
|
92
|
+
<AlertDialogAction
|
|
93
|
+
onClick={handleDelete}
|
|
94
|
+
disabled={isLoading}
|
|
95
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 px-6 shadow-sm shadow-destructive/20 border-0"
|
|
96
|
+
>
|
|
97
|
+
{isLoading ? "Deleting..." : "Delete Permanently"}
|
|
98
|
+
</AlertDialogAction>
|
|
99
|
+
</AlertDialogFooter>
|
|
100
|
+
</AlertDialogContent>
|
|
101
|
+
</AlertDialog>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { Label } from "@/components/ui/label";
|
|
8
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
DialogTrigger,
|
|
17
|
+
} from "@/components/ui/dialog";
|
|
18
|
+
import { Edit, Globe, Palette, Type, AlignLeft, CheckCircle2 } from "lucide-react";
|
|
19
|
+
import { useEnvironments } from "@/hooks/use-environments";
|
|
20
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
21
|
+
|
|
22
|
+
interface EditEnvironmentDialogProps {
|
|
23
|
+
environmentId: string;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function EditEnvironmentDialog({ environmentId, children }: EditEnvironmentDialogProps) {
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const [name, setName] = useState("");
|
|
30
|
+
const [description, setDescription] = useState("");
|
|
31
|
+
const [color, setColor] = useState("#3b82f6");
|
|
32
|
+
const [isDefault, setIsDefault] = useState(false);
|
|
33
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
34
|
+
const [error, setError] = useState("");
|
|
35
|
+
const { environments } = useEnvironments();
|
|
36
|
+
const queryClient = useQueryClient();
|
|
37
|
+
|
|
38
|
+
const environment = environments?.find(e => e.id === environmentId);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (environment && open) {
|
|
42
|
+
setName(environment.name);
|
|
43
|
+
setDescription(environment.description || "");
|
|
44
|
+
setColor(environment.color || "#3b82f6");
|
|
45
|
+
setIsDefault(environment.isDefault || false);
|
|
46
|
+
}
|
|
47
|
+
}, [environment, open]);
|
|
48
|
+
|
|
49
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
setError("");
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch('/api/environments', {
|
|
56
|
+
method: 'PUT',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({ id: environmentId, name, description, color, isDefault }),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
|
|
65
|
+
if (response.ok) {
|
|
66
|
+
setOpen(false);
|
|
67
|
+
queryClient.invalidateQueries({ queryKey: ["environments"] });
|
|
68
|
+
} else {
|
|
69
|
+
setError(data.error || "Failed to update environment");
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
setError("An unexpected error occurred");
|
|
73
|
+
} finally {
|
|
74
|
+
setIsLoading(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
80
|
+
<DialogTrigger asChild>
|
|
81
|
+
{children || (
|
|
82
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
83
|
+
<Edit className="h-4 w-4" />
|
|
84
|
+
</Button>
|
|
85
|
+
)}
|
|
86
|
+
</DialogTrigger>
|
|
87
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
88
|
+
<DialogHeader>
|
|
89
|
+
<div className="flex items-center gap-2 mb-1">
|
|
90
|
+
<div className="p-1.5 bg-primary/10 rounded-md">
|
|
91
|
+
<Globe className="h-5 w-5 text-primary" />
|
|
92
|
+
</div>
|
|
93
|
+
<DialogTitle className="text-xl">Edit Environment</DialogTitle>
|
|
94
|
+
</div>
|
|
95
|
+
<DialogDescription>
|
|
96
|
+
Update configuration and appearance for the {environment?.name} stage.
|
|
97
|
+
</DialogDescription>
|
|
98
|
+
</DialogHeader>
|
|
99
|
+
<form onSubmit={handleSubmit} className="space-y-6 pt-4">
|
|
100
|
+
{error && (
|
|
101
|
+
<div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
|
|
102
|
+
{error}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
<div className="space-y-4">
|
|
107
|
+
<div className="grid gap-2">
|
|
108
|
+
<Label htmlFor="name" className="flex items-center gap-2">
|
|
109
|
+
<Type className="h-3.5 w-3.5 text-muted-foreground" />
|
|
110
|
+
Environment Name
|
|
111
|
+
</Label>
|
|
112
|
+
<Input
|
|
113
|
+
id="name"
|
|
114
|
+
value={name}
|
|
115
|
+
onChange={(e) => setName(e.target.value)}
|
|
116
|
+
placeholder="e.g. production, staging"
|
|
117
|
+
required
|
|
118
|
+
disabled={isLoading}
|
|
119
|
+
className="bg-muted/30 border-muted"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div className="grid gap-2">
|
|
124
|
+
<Label htmlFor="description" className="flex items-center gap-2">
|
|
125
|
+
<AlignLeft className="h-3.5 w-3.5 text-muted-foreground" />
|
|
126
|
+
Description <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
|
|
127
|
+
</Label>
|
|
128
|
+
<Textarea
|
|
129
|
+
id="description"
|
|
130
|
+
value={description}
|
|
131
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
132
|
+
className="resize-none min-h-[100px] bg-muted/30 border-muted"
|
|
133
|
+
disabled={isLoading}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div className="grid gap-2">
|
|
138
|
+
<Label htmlFor="color" className="flex items-center gap-2">
|
|
139
|
+
<Palette className="h-3.5 w-3.5 text-muted-foreground" />
|
|
140
|
+
Environment Color
|
|
141
|
+
</Label>
|
|
142
|
+
<div className="flex items-center gap-4">
|
|
143
|
+
<Input
|
|
144
|
+
id="color"
|
|
145
|
+
type="color"
|
|
146
|
+
value={color}
|
|
147
|
+
onChange={(e) => setColor(e.target.value)}
|
|
148
|
+
className="w-14 h-11 p-1 rounded-md border-muted cursor-pointer"
|
|
149
|
+
disabled={isLoading}
|
|
150
|
+
/>
|
|
151
|
+
<div className="flex-1 px-3 py-2 bg-muted/30 border border-muted rounded-md flex items-center justify-between">
|
|
152
|
+
<span className="text-sm font-mono uppercase font-semibold text-muted-foreground">
|
|
153
|
+
{color}
|
|
154
|
+
</span>
|
|
155
|
+
<div
|
|
156
|
+
className="w-4 h-4 rounded-full shadow-sm"
|
|
157
|
+
style={{ backgroundColor: color }}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div className="flex items-center space-x-3 p-3 bg-muted/30 rounded-lg border border-muted">
|
|
164
|
+
<Checkbox
|
|
165
|
+
id="isDefault"
|
|
166
|
+
checked={isDefault}
|
|
167
|
+
onCheckedChange={(checked) => setIsDefault(!!checked)}
|
|
168
|
+
disabled={isLoading}
|
|
169
|
+
/>
|
|
170
|
+
<div className="grid gap-1.5 leading-none">
|
|
171
|
+
<Label
|
|
172
|
+
htmlFor="isDefault"
|
|
173
|
+
className="text-sm font-medium leading-none cursor-pointer flex items-center gap-2"
|
|
174
|
+
>
|
|
175
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-primary" />
|
|
176
|
+
Set as Primary
|
|
177
|
+
</Label>
|
|
178
|
+
<p className="text-xs text-muted-foreground">
|
|
179
|
+
Variables are assigned to the primary environment by default.
|
|
180
|
+
</p>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<DialogFooter className="pt-2">
|
|
186
|
+
<Button
|
|
187
|
+
type="button"
|
|
188
|
+
variant="ghost"
|
|
189
|
+
onClick={() => setOpen(false)}
|
|
190
|
+
disabled={isLoading}
|
|
191
|
+
>
|
|
192
|
+
Cancel
|
|
193
|
+
</Button>
|
|
194
|
+
<Button type="submit" disabled={isLoading} className="px-8 shadow-sm">
|
|
195
|
+
{isLoading ? "Updating..." : "Update Environment"}
|
|
196
|
+
</Button>
|
|
197
|
+
</DialogFooter>
|
|
198
|
+
</form>
|
|
199
|
+
</DialogContent>
|
|
200
|
+
</Dialog>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { Label } from "@/components/ui/label";
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
DialogTrigger,
|
|
16
|
+
} from "@/components/ui/dialog";
|
|
17
|
+
import { Edit, User, Mail, Shield, UserRoundPen } from "lucide-react";
|
|
18
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
19
|
+
import {
|
|
20
|
+
Select,
|
|
21
|
+
SelectContent,
|
|
22
|
+
SelectItem,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue,
|
|
25
|
+
} from "@/components/ui/select";
|
|
26
|
+
|
|
27
|
+
interface EditMemberDialogProps {
|
|
28
|
+
teamId: string;
|
|
29
|
+
member: {
|
|
30
|
+
id: string;
|
|
31
|
+
role: string;
|
|
32
|
+
user: {
|
|
33
|
+
name: string;
|
|
34
|
+
email: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function EditMemberDialog({ teamId, member, children }: EditMemberDialogProps) {
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const [role, setRole] = useState(member.role);
|
|
43
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
44
|
+
const [error, setError] = useState("");
|
|
45
|
+
const queryClient = useQueryClient();
|
|
46
|
+
|
|
47
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
setError("");
|
|
50
|
+
setIsLoading(true);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch('/api/team-members', {
|
|
54
|
+
method: 'PUT',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({ teamId, memberId: member.id, role }),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
|
|
63
|
+
if (response.ok) {
|
|
64
|
+
setOpen(false);
|
|
65
|
+
queryClient.invalidateQueries({ queryKey: ["team-members", teamId] });
|
|
66
|
+
} else {
|
|
67
|
+
setError(data.error || "Failed to update member");
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
setError("An unexpected error occurred");
|
|
71
|
+
} finally {
|
|
72
|
+
setIsLoading(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
78
|
+
<DialogTrigger asChild>
|
|
79
|
+
{children || (
|
|
80
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
81
|
+
<Edit className="h-4 w-4" />
|
|
82
|
+
</Button>
|
|
83
|
+
)}
|
|
84
|
+
</DialogTrigger>
|
|
85
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
86
|
+
<DialogHeader>
|
|
87
|
+
<div className="flex items-center gap-2 mb-1">
|
|
88
|
+
<div className="p-1.5 bg-primary/10 rounded-md">
|
|
89
|
+
<UserRoundPen className="h-5 w-5 text-primary" />
|
|
90
|
+
</div>
|
|
91
|
+
<DialogTitle className="text-xl">Edit Member Role</DialogTitle>
|
|
92
|
+
</div>
|
|
93
|
+
<DialogDescription>
|
|
94
|
+
Change role for {member.user.name} ({member.user.email})
|
|
95
|
+
</DialogDescription>
|
|
96
|
+
</DialogHeader>
|
|
97
|
+
|
|
98
|
+
<form onSubmit={handleSubmit} className="space-y-4 py-4">
|
|
99
|
+
{error && (
|
|
100
|
+
<div className="p-3 text-sm font-medium text-destructive bg-destructive/10 rounded-lg border border-destructive/20 text-center">
|
|
101
|
+
{error}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
<div className="space-y-2">
|
|
106
|
+
<Label htmlFor="role" className="text-sm font-medium text-slate-700">Role</Label>
|
|
107
|
+
<Select value={role} onValueChange={setRole}>
|
|
108
|
+
<SelectTrigger className="bg-white border-slate-200">
|
|
109
|
+
<SelectValue placeholder="Select role" />
|
|
110
|
+
</SelectTrigger>
|
|
111
|
+
<SelectContent>
|
|
112
|
+
<SelectItem value="member">Member</SelectItem>
|
|
113
|
+
<SelectItem value="admin">Admin</SelectItem>
|
|
114
|
+
<SelectItem value="owner">Owner</SelectItem>
|
|
115
|
+
</SelectContent>
|
|
116
|
+
</Select>
|
|
117
|
+
<p className="text-[11px] text-slate-500 mt-1">
|
|
118
|
+
Admin can manage environments and secrets. Owner can also manage team members.
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<DialogFooter className="pt-2">
|
|
123
|
+
<Button
|
|
124
|
+
type="button"
|
|
125
|
+
variant="outline"
|
|
126
|
+
onClick={() => setOpen(false)}
|
|
127
|
+
className="border-slate-200 hover:bg-slate-50"
|
|
128
|
+
>
|
|
129
|
+
Cancel
|
|
130
|
+
</Button>
|
|
131
|
+
<Button
|
|
132
|
+
type="submit"
|
|
133
|
+
disabled={isLoading || role === member.role}
|
|
134
|
+
className="bg-slate-950 hover:bg-slate-800 text-white min-w-[100px]"
|
|
135
|
+
>
|
|
136
|
+
{isLoading ? "Saving..." : "Save Changes"}
|
|
137
|
+
</Button>
|
|
138
|
+
</DialogFooter>
|
|
139
|
+
</form>
|
|
140
|
+
</DialogContent>
|
|
141
|
+
</Dialog>
|
|
142
|
+
);
|
|
143
|
+
}
|