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.
Files changed (123) hide show
  1. package/README.md +112 -0
  2. package/components.json +22 -0
  3. package/dist/nobalmako.js +272 -0
  4. package/drizzle/0000_pink_spiral.sql +126 -0
  5. package/drizzle/meta/0000_snapshot.json +1027 -0
  6. package/drizzle/meta/_journal.json +13 -0
  7. package/drizzle.config.ts +10 -0
  8. package/eslint.config.mjs +18 -0
  9. package/next.config.ts +7 -0
  10. package/package.json +80 -0
  11. package/postcss.config.mjs +7 -0
  12. package/public/file.svg +1 -0
  13. package/public/globe.svg +1 -0
  14. package/public/next.svg +1 -0
  15. package/public/vercel.svg +1 -0
  16. package/public/window.svg +1 -0
  17. package/server/index.ts +118 -0
  18. package/src/app/api/api-keys/[id]/route.ts +147 -0
  19. package/src/app/api/api-keys/route.ts +151 -0
  20. package/src/app/api/audit-logs/route.ts +84 -0
  21. package/src/app/api/auth/forgot-password/route.ts +47 -0
  22. package/src/app/api/auth/login/route.ts +99 -0
  23. package/src/app/api/auth/logout/route.ts +15 -0
  24. package/src/app/api/auth/me/route.ts +23 -0
  25. package/src/app/api/auth/mfa/setup/route.ts +33 -0
  26. package/src/app/api/auth/mfa/verify/route.ts +45 -0
  27. package/src/app/api/auth/register/route.ts +140 -0
  28. package/src/app/api/auth/reset-password/route.ts +52 -0
  29. package/src/app/api/auth/update/route.ts +71 -0
  30. package/src/app/api/auth/verify/route.ts +39 -0
  31. package/src/app/api/environments/route.ts +227 -0
  32. package/src/app/api/team-members/route.ts +385 -0
  33. package/src/app/api/teams/route.ts +217 -0
  34. package/src/app/api/variable-history/route.ts +218 -0
  35. package/src/app/api/variables/route.ts +476 -0
  36. package/src/app/api/webhooks/route.ts +77 -0
  37. package/src/app/api-keys/APIKeysClient.tsx +316 -0
  38. package/src/app/api-keys/page.tsx +10 -0
  39. package/src/app/api-reference/page.tsx +324 -0
  40. package/src/app/audit-log/AuditLogClient.tsx +229 -0
  41. package/src/app/audit-log/page.tsx +10 -0
  42. package/src/app/auth/forgot-password/page.tsx +121 -0
  43. package/src/app/auth/login/LoginForm.tsx +145 -0
  44. package/src/app/auth/login/page.tsx +11 -0
  45. package/src/app/auth/register/RegisterForm.tsx +156 -0
  46. package/src/app/auth/register/page.tsx +16 -0
  47. package/src/app/auth/reset-password/page.tsx +160 -0
  48. package/src/app/dashboard/DashboardClient.tsx +219 -0
  49. package/src/app/dashboard/page.tsx +11 -0
  50. package/src/app/docs/page.tsx +251 -0
  51. package/src/app/favicon.ico +0 -0
  52. package/src/app/globals.css +123 -0
  53. package/src/app/layout.tsx +35 -0
  54. package/src/app/page.tsx +231 -0
  55. package/src/app/profile/ProfileClient.tsx +230 -0
  56. package/src/app/profile/page.tsx +10 -0
  57. package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
  58. package/src/app/project/[id]/page.tsx +17 -0
  59. package/src/bin/nobalmako.ts +341 -0
  60. package/src/components/ApiKeysManager.tsx +529 -0
  61. package/src/components/AppLayout.tsx +193 -0
  62. package/src/components/BulkActions.tsx +138 -0
  63. package/src/components/CreateEnvironmentDialog.tsx +207 -0
  64. package/src/components/CreateTeamDialog.tsx +174 -0
  65. package/src/components/CreateVariableDialog.tsx +311 -0
  66. package/src/components/DeleteEnvironmentDialog.tsx +104 -0
  67. package/src/components/DeleteTeamDialog.tsx +112 -0
  68. package/src/components/DeleteVariableDialog.tsx +103 -0
  69. package/src/components/EditEnvironmentDialog.tsx +202 -0
  70. package/src/components/EditMemberDialog.tsx +143 -0
  71. package/src/components/EditTeamDialog.tsx +178 -0
  72. package/src/components/EditVariableDialog.tsx +231 -0
  73. package/src/components/ImportVariablesDialog.tsx +347 -0
  74. package/src/components/InviteMemberDialog.tsx +191 -0
  75. package/src/components/LeaveProjectDialog.tsx +111 -0
  76. package/src/components/MFASettings.tsx +136 -0
  77. package/src/components/ProjectDiff.tsx +123 -0
  78. package/src/components/Providers.tsx +24 -0
  79. package/src/components/RemoveMemberDialog.tsx +112 -0
  80. package/src/components/SearchDialog.tsx +276 -0
  81. package/src/components/SecurityOverview.tsx +92 -0
  82. package/src/components/TeamMembersManager.tsx +103 -0
  83. package/src/components/VariableHistoryDialog.tsx +265 -0
  84. package/src/components/WebhooksManager.tsx +169 -0
  85. package/src/components/ui/alert-dialog.tsx +160 -0
  86. package/src/components/ui/alert.tsx +59 -0
  87. package/src/components/ui/avatar.tsx +53 -0
  88. package/src/components/ui/badge.tsx +46 -0
  89. package/src/components/ui/button.tsx +62 -0
  90. package/src/components/ui/card.tsx +92 -0
  91. package/src/components/ui/checkbox.tsx +32 -0
  92. package/src/components/ui/dialog.tsx +143 -0
  93. package/src/components/ui/dropdown-menu.tsx +257 -0
  94. package/src/components/ui/input.tsx +21 -0
  95. package/src/components/ui/label.tsx +24 -0
  96. package/src/components/ui/select.tsx +190 -0
  97. package/src/components/ui/separator.tsx +28 -0
  98. package/src/components/ui/sonner.tsx +37 -0
  99. package/src/components/ui/switch.tsx +31 -0
  100. package/src/components/ui/table.tsx +117 -0
  101. package/src/components/ui/tabs.tsx +66 -0
  102. package/src/components/ui/textarea.tsx +18 -0
  103. package/src/hooks/use-api-keys.ts +95 -0
  104. package/src/hooks/use-audit-logs.ts +58 -0
  105. package/src/hooks/use-auth.tsx +121 -0
  106. package/src/hooks/use-environments.ts +33 -0
  107. package/src/hooks/use-project-permissions.ts +49 -0
  108. package/src/hooks/use-team-members.ts +30 -0
  109. package/src/hooks/use-teams.ts +33 -0
  110. package/src/hooks/use-variables.ts +38 -0
  111. package/src/lib/audit.ts +36 -0
  112. package/src/lib/auth.ts +108 -0
  113. package/src/lib/crypto.ts +39 -0
  114. package/src/lib/db.ts +15 -0
  115. package/src/lib/dynamic-providers.ts +19 -0
  116. package/src/lib/email.ts +110 -0
  117. package/src/lib/mail.ts +51 -0
  118. package/src/lib/permissions.ts +51 -0
  119. package/src/lib/schema.ts +240 -0
  120. package/src/lib/seed.ts +107 -0
  121. package/src/lib/utils.ts +6 -0
  122. package/src/lib/webhooks.ts +42 -0
  123. 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">&quot;{environmentName}&quot;</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
+ }