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,174 @@
|
|
|
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 { 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 { Plus, Shield, Palette, Type, AlignLeft } from "lucide-react";
|
|
19
|
+
import { useTeams } from "@/hooks/use-teams";
|
|
20
|
+
|
|
21
|
+
interface CreateTeamDialogProps {
|
|
22
|
+
children?: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function CreateTeamDialog({ children }: CreateTeamDialogProps) {
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
const [name, setName] = useState("");
|
|
28
|
+
const [description, setDescription] = useState("");
|
|
29
|
+
const [color, setColor] = useState("#3b82f6");
|
|
30
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
31
|
+
const [error, setError] = useState("");
|
|
32
|
+
const queryClient = useQueryClient();
|
|
33
|
+
|
|
34
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
setError("");
|
|
37
|
+
setIsLoading(true);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch('/api/teams', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({ name, description, color }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
|
|
50
|
+
if (response.ok) {
|
|
51
|
+
setOpen(false);
|
|
52
|
+
setName("");
|
|
53
|
+
setDescription("");
|
|
54
|
+
setColor("#3b82f6");
|
|
55
|
+
// Invalidate and refetch teams
|
|
56
|
+
queryClient.invalidateQueries({ queryKey: ['teams'] });
|
|
57
|
+
} else {
|
|
58
|
+
setError(data.error || "Failed to create team");
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
setError("An unexpected error occurred");
|
|
62
|
+
} finally {
|
|
63
|
+
setIsLoading(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
69
|
+
<DialogTrigger asChild>
|
|
70
|
+
{children || (
|
|
71
|
+
<Button className="shadow-sm">
|
|
72
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
73
|
+
New Project
|
|
74
|
+
</Button>
|
|
75
|
+
)}
|
|
76
|
+
</DialogTrigger>
|
|
77
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
78
|
+
<DialogHeader>
|
|
79
|
+
<div className="flex items-center gap-2 mb-1">
|
|
80
|
+
<div className="p-1.5 bg-primary/10 rounded-md">
|
|
81
|
+
<Shield className="h-5 w-5 text-primary" />
|
|
82
|
+
</div>
|
|
83
|
+
<DialogTitle className="text-xl">Create New Project</DialogTitle>
|
|
84
|
+
</div>
|
|
85
|
+
<DialogDescription>
|
|
86
|
+
Setting up a new project helps you organize variables by application or service.
|
|
87
|
+
</DialogDescription>
|
|
88
|
+
</DialogHeader>
|
|
89
|
+
<form onSubmit={handleSubmit} className="space-y-6 pt-4">
|
|
90
|
+
{error && (
|
|
91
|
+
<div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
|
|
92
|
+
{error}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
<div className="space-y-4">
|
|
97
|
+
<div className="grid gap-2">
|
|
98
|
+
<Label htmlFor="name" className="flex items-center gap-2">
|
|
99
|
+
<Type className="h-3.5 w-3.5 text-muted-foreground" />
|
|
100
|
+
Project Name
|
|
101
|
+
</Label>
|
|
102
|
+
<Input
|
|
103
|
+
id="name"
|
|
104
|
+
value={name}
|
|
105
|
+
onChange={(e) => setName(e.target.value)}
|
|
106
|
+
placeholder="e.g. Acme API, Frontend Dashboard"
|
|
107
|
+
required
|
|
108
|
+
disabled={isLoading}
|
|
109
|
+
className="bg-muted/30 border-muted"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className="grid gap-2">
|
|
114
|
+
<Label htmlFor="description" className="flex items-center gap-2">
|
|
115
|
+
<AlignLeft className="h-3.5 w-3.5 text-muted-foreground" />
|
|
116
|
+
Description <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
|
|
117
|
+
</Label>
|
|
118
|
+
<Textarea
|
|
119
|
+
id="description"
|
|
120
|
+
value={description}
|
|
121
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
122
|
+
placeholder="What service or application is this for?"
|
|
123
|
+
className="resize-none min-h-[100px] bg-muted/30 border-muted"
|
|
124
|
+
disabled={isLoading}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div className="grid gap-2">
|
|
129
|
+
<Label htmlFor="color" className="flex items-center gap-2">
|
|
130
|
+
<Palette className="h-3.5 w-3.5 text-muted-foreground" />
|
|
131
|
+
Identity Color
|
|
132
|
+
</Label>
|
|
133
|
+
<div className="flex items-center gap-4">
|
|
134
|
+
<div className="relative">
|
|
135
|
+
<Input
|
|
136
|
+
id="color"
|
|
137
|
+
type="color"
|
|
138
|
+
value={color}
|
|
139
|
+
onChange={(e) => setColor(e.target.value)}
|
|
140
|
+
className="w-14 h-11 p-1 rounded-md border-muted cursor-pointer"
|
|
141
|
+
disabled={isLoading}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="flex-1 px-3 py-2 bg-muted/30 border border-muted rounded-md flex items-center justify-between">
|
|
145
|
+
<span className="text-sm font-mono uppercase font-semibold text-muted-foreground">
|
|
146
|
+
{color}
|
|
147
|
+
</span>
|
|
148
|
+
<div
|
|
149
|
+
className="w-4 h-4 rounded-full shadow-sm"
|
|
150
|
+
style={{ backgroundColor: color }}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<DialogFooter className="pt-2">
|
|
158
|
+
<Button
|
|
159
|
+
type="button"
|
|
160
|
+
variant="ghost"
|
|
161
|
+
onClick={() => setOpen(false)}
|
|
162
|
+
disabled={isLoading}
|
|
163
|
+
>
|
|
164
|
+
Cancel
|
|
165
|
+
</Button>
|
|
166
|
+
<Button type="submit" disabled={isLoading} className="px-8">
|
|
167
|
+
{isLoading ? "Creating..." : "Create Project"}
|
|
168
|
+
</Button>
|
|
169
|
+
</DialogFooter>
|
|
170
|
+
</form>
|
|
171
|
+
</DialogContent>
|
|
172
|
+
</Dialog>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
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 { 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 {
|
|
19
|
+
Select,
|
|
20
|
+
SelectContent,
|
|
21
|
+
SelectItem,
|
|
22
|
+
SelectTrigger,
|
|
23
|
+
SelectValue,
|
|
24
|
+
} from "@/components/ui/select";
|
|
25
|
+
import { Plus, Eye, EyeOff, Database, Key, Type, AlignLeft, ShieldCheck, Layers, Calendar } from "lucide-react";
|
|
26
|
+
import { useEnvironments } from "@/hooks/use-environments";
|
|
27
|
+
import { useVariables } from "@/hooks/use-variables";
|
|
28
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
29
|
+
import { Switch } from "@/components/ui/switch";
|
|
30
|
+
|
|
31
|
+
interface CreateVariableDialogProps {
|
|
32
|
+
environmentId?: string;
|
|
33
|
+
children?: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function CreateVariableDialog({ environmentId, children }: CreateVariableDialogProps) {
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
const [key, setKey] = useState("");
|
|
39
|
+
const [value, setValue] = useState("");
|
|
40
|
+
const [description, setDescription] = useState("");
|
|
41
|
+
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(environmentId || "");
|
|
42
|
+
const [isSecret, setIsSecret] = useState(true);
|
|
43
|
+
const [isDynamic, setIsDynamic] = useState(false);
|
|
44
|
+
const [provider, setProvider] = useState("");
|
|
45
|
+
const [expiresAt, setExpiresAt] = useState("");
|
|
46
|
+
const [showValue, setShowValue] = useState(false);
|
|
47
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
48
|
+
const [error, setError] = useState("");
|
|
49
|
+
const { environments } = useEnvironments();
|
|
50
|
+
const queryClient = useQueryClient();
|
|
51
|
+
|
|
52
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
setError("");
|
|
55
|
+
setIsLoading(true);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const selectedEnv = environments?.find(e => e.id === selectedEnvironmentId);
|
|
59
|
+
|
|
60
|
+
if (!selectedEnv) {
|
|
61
|
+
setError("Please select an environment");
|
|
62
|
+
setIsLoading(false);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const response = await fetch('/api/variables', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
key,
|
|
73
|
+
value,
|
|
74
|
+
description,
|
|
75
|
+
environmentId: selectedEnvironmentId,
|
|
76
|
+
teamId: selectedEnv.teamId,
|
|
77
|
+
isSecret,
|
|
78
|
+
isDynamic,
|
|
79
|
+
provider: isDynamic ? provider : null,
|
|
80
|
+
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
|
|
86
|
+
if (response.ok) {
|
|
87
|
+
setOpen(false);
|
|
88
|
+
setKey("");
|
|
89
|
+
setValue("");
|
|
90
|
+
setDescription("");
|
|
91
|
+
if (!environmentId) setSelectedEnvironmentId("");
|
|
92
|
+
setIsSecret(true);
|
|
93
|
+
setExpiresAt("");
|
|
94
|
+
setShowValue(false);
|
|
95
|
+
queryClient.invalidateQueries({ queryKey: ['variables'] });
|
|
96
|
+
} else {
|
|
97
|
+
setError(data.error || "Failed to create variable");
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
setError("An unexpected error occurred");
|
|
101
|
+
} finally {
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
108
|
+
<DialogTrigger asChild>
|
|
109
|
+
{children || (
|
|
110
|
+
<Button className="shadow-sm">
|
|
111
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
112
|
+
New Variable
|
|
113
|
+
</Button>
|
|
114
|
+
)}
|
|
115
|
+
</DialogTrigger>
|
|
116
|
+
<DialogContent className="sm:max-w-[500px]">
|
|
117
|
+
<DialogHeader>
|
|
118
|
+
<div className="flex items-center gap-2 mb-1">
|
|
119
|
+
<div className="p-1.5 bg-primary/10 rounded-md">
|
|
120
|
+
<Database className="h-5 w-5 text-primary" />
|
|
121
|
+
</div>
|
|
122
|
+
<DialogTitle className="text-xl">Create New Variable</DialogTitle>
|
|
123
|
+
</div>
|
|
124
|
+
<DialogDescription>
|
|
125
|
+
Add a new environment variable or secret to your project configuration.
|
|
126
|
+
</DialogDescription>
|
|
127
|
+
</DialogHeader>
|
|
128
|
+
<form onSubmit={handleSubmit} className="space-y-6 pt-4">
|
|
129
|
+
{error && (
|
|
130
|
+
<div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
|
|
131
|
+
{error}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
<div className="space-y-4">
|
|
136
|
+
{!environmentId && (
|
|
137
|
+
<div className="grid gap-2">
|
|
138
|
+
<Label htmlFor="environment" className="flex items-center gap-2">
|
|
139
|
+
<Layers className="h-3.5 w-3.5 text-muted-foreground" />
|
|
140
|
+
Target Environment
|
|
141
|
+
</Label>
|
|
142
|
+
<Select value={selectedEnvironmentId} onValueChange={setSelectedEnvironmentId} required>
|
|
143
|
+
<SelectTrigger className="bg-muted/30 border-muted">
|
|
144
|
+
<SelectValue placeholder="Select an environment" />
|
|
145
|
+
</SelectTrigger>
|
|
146
|
+
<SelectContent>
|
|
147
|
+
{environments?.map((env) => (
|
|
148
|
+
<SelectItem key={env.id} value={env.id}>
|
|
149
|
+
{env.name} ({env.teamName})
|
|
150
|
+
</SelectItem>
|
|
151
|
+
))}
|
|
152
|
+
</SelectContent>
|
|
153
|
+
</Select>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
<div className="grid gap-2">
|
|
158
|
+
<Label htmlFor="key" className="flex items-center gap-2">
|
|
159
|
+
<Type className="h-3.5 w-3.5 text-muted-foreground" />
|
|
160
|
+
Variable Key
|
|
161
|
+
</Label>
|
|
162
|
+
<Input
|
|
163
|
+
id="key"
|
|
164
|
+
value={key}
|
|
165
|
+
onChange={(e) => setKey(e.target.value)}
|
|
166
|
+
placeholder="e.g. DATABASE_URL, API_KEY"
|
|
167
|
+
required
|
|
168
|
+
disabled={isLoading}
|
|
169
|
+
className="bg-muted/30 border-muted font-mono text-sm"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="grid gap-2">
|
|
174
|
+
<div className="flex items-center justify-between">
|
|
175
|
+
<Label htmlFor="value" className="flex items-center gap-2">
|
|
176
|
+
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
|
177
|
+
Value
|
|
178
|
+
</Label>
|
|
179
|
+
{isSecret && (
|
|
180
|
+
<Button
|
|
181
|
+
type="button"
|
|
182
|
+
variant="ghost"
|
|
183
|
+
size="sm"
|
|
184
|
+
className="h-7 px-2 text-xs text-muted-foreground"
|
|
185
|
+
onClick={() => setShowValue(!showValue)}
|
|
186
|
+
disabled={isLoading}
|
|
187
|
+
>
|
|
188
|
+
{showValue ? <EyeOff className="h-3.5 w-3.5 mr-1.5" /> : <Eye className="h-3.5 w-3.5 mr-1.5" />}
|
|
189
|
+
{showValue ? "Hide" : "Show"}
|
|
190
|
+
</Button>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
<div className="relative">
|
|
194
|
+
<Input
|
|
195
|
+
id="value"
|
|
196
|
+
type={isSecret && !showValue ? "password" : "text"}
|
|
197
|
+
value={value}
|
|
198
|
+
onChange={(e) => setValue(e.target.value)}
|
|
199
|
+
placeholder="Enter variable value"
|
|
200
|
+
required
|
|
201
|
+
disabled={isLoading}
|
|
202
|
+
className="bg-muted/30 border-muted font-mono text-sm"
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div className="grid gap-2">
|
|
208
|
+
<Label htmlFor="description" className="flex items-center gap-2">
|
|
209
|
+
<AlignLeft className="h-3.5 w-3.5 text-muted-foreground" />
|
|
210
|
+
Description <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
|
|
211
|
+
</Label>
|
|
212
|
+
<Input
|
|
213
|
+
id="description"
|
|
214
|
+
value={description}
|
|
215
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
216
|
+
placeholder="What is this variable for?"
|
|
217
|
+
disabled={isLoading}
|
|
218
|
+
className="bg-muted/30 border-muted"
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div className="grid gap-2">
|
|
223
|
+
<Label htmlFor="expiresAt" className="flex items-center gap-2">
|
|
224
|
+
<Calendar className="h-3.5 w-3.5 text-muted-foreground" />
|
|
225
|
+
Expiration Date <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
|
|
226
|
+
</Label>
|
|
227
|
+
<Input
|
|
228
|
+
id="expiresAt"
|
|
229
|
+
type="date"
|
|
230
|
+
value={expiresAt}
|
|
231
|
+
onChange={(e) => setExpiresAt(e.target.value)}
|
|
232
|
+
disabled={isLoading}
|
|
233
|
+
className="bg-muted/30 border-muted"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div className="flex items-center justify-between p-3 bg-blue-500/5 rounded-lg border border-blue-500/10">
|
|
238
|
+
<div className="grid gap-1.5 leading-none">
|
|
239
|
+
<Label htmlFor="isDynamic" className="text-sm font-medium leading-none cursor-pointer flex items-center gap-2">
|
|
240
|
+
<ShieldCheck className="h-3.5 w-3.5 text-blue-500" />
|
|
241
|
+
Dynamic Secret Provider
|
|
242
|
+
</Label>
|
|
243
|
+
<p className="text-xs text-muted-foreground">
|
|
244
|
+
Fetch value on-demand from AWS or Vault.
|
|
245
|
+
</p>
|
|
246
|
+
</div>
|
|
247
|
+
<Switch
|
|
248
|
+
id="isDynamic"
|
|
249
|
+
checked={isDynamic}
|
|
250
|
+
onCheckedChange={setIsDynamic}
|
|
251
|
+
disabled={isLoading}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{isDynamic && (
|
|
256
|
+
<div className="grid gap-2 animate-in fade-in slide-in-from-top-1">
|
|
257
|
+
<Label htmlFor="provider">Provider</Label>
|
|
258
|
+
<Select value={provider} onValueChange={setProvider} required>
|
|
259
|
+
<SelectTrigger className="bg-muted/30 border-muted">
|
|
260
|
+
<SelectValue placeholder="Select provider" />
|
|
261
|
+
</SelectTrigger>
|
|
262
|
+
<SelectContent>
|
|
263
|
+
<SelectItem value="aws">AWS Secrets Manager</SelectItem>
|
|
264
|
+
<SelectItem value="vault">HashiCorp Vault</SelectItem>
|
|
265
|
+
</SelectContent>
|
|
266
|
+
</Select>
|
|
267
|
+
<p className="text-[10px] text-muted-foreground">The "Value" field above should be the ARN or Path to the secret.</p>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
<div className="flex items-center space-x-3 p-3 bg-primary/5 rounded-lg border border-primary/10">
|
|
272
|
+
<Checkbox
|
|
273
|
+
id="isSecret"
|
|
274
|
+
checked={isSecret}
|
|
275
|
+
onCheckedChange={(checked) => setIsSecret(!!checked)}
|
|
276
|
+
disabled={isLoading}
|
|
277
|
+
className="data-[state=checked]:bg-primary"
|
|
278
|
+
/>
|
|
279
|
+
<div className="grid gap-1.5 leading-none">
|
|
280
|
+
<Label
|
|
281
|
+
htmlFor="isSecret"
|
|
282
|
+
className="text-sm font-medium leading-none cursor-pointer flex items-center gap-2"
|
|
283
|
+
>
|
|
284
|
+
<ShieldCheck className="h-3.5 w-3.5 text-primary" />
|
|
285
|
+
Encrypt as Secret
|
|
286
|
+
</Label>
|
|
287
|
+
<p className="text-xs text-muted-foreground">
|
|
288
|
+
Secrets are encrypted at rest and masked in the UI by default.
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<DialogFooter className="pt-2">
|
|
295
|
+
<Button
|
|
296
|
+
type="button"
|
|
297
|
+
variant="ghost"
|
|
298
|
+
onClick={() => setOpen(false)}
|
|
299
|
+
disabled={isLoading}
|
|
300
|
+
>
|
|
301
|
+
Cancel
|
|
302
|
+
</Button>
|
|
303
|
+
<Button type="submit" disabled={isLoading || (!environmentId && !selectedEnvironmentId)} className="px-8 shadow-sm">
|
|
304
|
+
{isLoading ? "Creating..." : "Create Variable"}
|
|
305
|
+
</Button>
|
|
306
|
+
</DialogFooter>
|
|
307
|
+
</form>
|
|
308
|
+
</DialogContent>
|
|
309
|
+
</Dialog>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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, ShieldAlert } from "lucide-react";
|
|
17
|
+
import { useEnvironments } from "@/hooks/use-environments";
|
|
18
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
19
|
+
|
|
20
|
+
interface DeleteEnvironmentDialogProps {
|
|
21
|
+
environmentId: string;
|
|
22
|
+
environmentName: string;
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function DeleteEnvironmentDialog({ environmentId, environmentName, children }: DeleteEnvironmentDialogProps) {
|
|
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/environments', {
|
|
39
|
+
method: 'DELETE',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({ id: environmentId }),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
setOpen(false);
|
|
50
|
+
queryClient.invalidateQueries({ queryKey: ["environments"] });
|
|
51
|
+
} else {
|
|
52
|
+
setError(data.error || "Failed to delete environment");
|
|
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 text-destructive">
|
|
74
|
+
<ShieldAlert className="h-5 w-5" />
|
|
75
|
+
</div>
|
|
76
|
+
<AlertDialogTitle className="text-xl">Delete Environment</AlertDialogTitle>
|
|
77
|
+
</div>
|
|
78
|
+
<AlertDialogDescription className="text-base text-muted-foreground pt-1">
|
|
79
|
+
Are you sure you want to delete the environment <span className="font-bold text-foreground bg-muted px-1.5 rounded">"{environmentName}"</span>?
|
|
80
|
+
<div className="mt-3 p-4 bg-destructive/5 rounded-lg border border-destructive/20 text-sm text-destructive-foreground font-medium flex items-start gap-2">
|
|
81
|
+
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" />
|
|
82
|
+
<span>DANGER: This will permanently delete ALL variables and historical data stored within this environment.</span>
|
|
83
|
+
</div>
|
|
84
|
+
</AlertDialogDescription>
|
|
85
|
+
</AlertDialogHeader>
|
|
86
|
+
{error && (
|
|
87
|
+
<div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20 my-2">
|
|
88
|
+
{error}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
<AlertDialogFooter className="pt-4 gap-2">
|
|
92
|
+
<AlertDialogCancel disabled={isLoading} className="mt-0">Cancel</AlertDialogCancel>
|
|
93
|
+
<AlertDialogAction
|
|
94
|
+
onClick={handleDelete}
|
|
95
|
+
disabled={isLoading}
|
|
96
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 px-6 shadow-sm shadow-destructive/20 border-0"
|
|
97
|
+
>
|
|
98
|
+
{isLoading ? "Deleting..." : "Confirm Deletion"}
|
|
99
|
+
</AlertDialogAction>
|
|
100
|
+
</AlertDialogFooter>
|
|
101
|
+
</AlertDialogContent>
|
|
102
|
+
</AlertDialog>
|
|
103
|
+
);
|
|
104
|
+
}
|