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,178 @@
|
|
|
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, Shield, Palette, Type, AlignLeft } from "lucide-react";
|
|
19
|
+
import { useTeams } from "@/hooks/use-teams";
|
|
20
|
+
|
|
21
|
+
interface EditTeamDialogProps {
|
|
22
|
+
teamId: string;
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function EditTeamDialog({ teamId, children }: EditTeamDialogProps) {
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const [name, setName] = useState("");
|
|
29
|
+
const [description, setDescription] = useState("");
|
|
30
|
+
const [color, setColor] = useState("#3b82f6");
|
|
31
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
32
|
+
const [error, setError] = useState("");
|
|
33
|
+
const { teams } = useTeams();
|
|
34
|
+
const queryClient = useQueryClient();
|
|
35
|
+
|
|
36
|
+
const team = teams?.find(t => t.id === teamId);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (team && open) {
|
|
40
|
+
setName(team.name);
|
|
41
|
+
setDescription(team.description || "");
|
|
42
|
+
setColor(team.color || "#3b82f6");
|
|
43
|
+
}
|
|
44
|
+
}, [team, open]);
|
|
45
|
+
|
|
46
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
setError("");
|
|
49
|
+
setIsLoading(true);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch('/api/teams', {
|
|
53
|
+
method: 'PUT',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({ id: teamId, name, description, color }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
setOpen(false);
|
|
64
|
+
queryClient.invalidateQueries({ queryKey: ['teams'] });
|
|
65
|
+
} else {
|
|
66
|
+
setError(data.error || "Failed to update team");
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
setError("An unexpected error occurred");
|
|
70
|
+
} finally {
|
|
71
|
+
setIsLoading(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
77
|
+
<DialogTrigger asChild>
|
|
78
|
+
{children || (
|
|
79
|
+
<Button variant="ghost" size="sm">
|
|
80
|
+
<Edit className="h-4 w-4" />
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
</DialogTrigger>
|
|
84
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
85
|
+
<DialogHeader>
|
|
86
|
+
<div className="flex items-center gap-2 mb-1">
|
|
87
|
+
<div className="p-1.5 bg-primary/10 rounded-md">
|
|
88
|
+
<Shield className="h-5 w-5 text-primary" />
|
|
89
|
+
</div>
|
|
90
|
+
<DialogTitle className="text-xl">Edit Project</DialogTitle>
|
|
91
|
+
</div>
|
|
92
|
+
<DialogDescription>
|
|
93
|
+
Update project metadata and branding for {team?.name}.
|
|
94
|
+
</DialogDescription>
|
|
95
|
+
</DialogHeader>
|
|
96
|
+
<form onSubmit={handleSubmit} className="space-y-6 pt-4">
|
|
97
|
+
{error && (
|
|
98
|
+
<div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
|
|
99
|
+
{error}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<div className="space-y-4">
|
|
104
|
+
<div className="grid gap-2">
|
|
105
|
+
<Label htmlFor="name" className="flex items-center gap-2">
|
|
106
|
+
<Type className="h-3.5 w-3.5 text-muted-foreground" />
|
|
107
|
+
Project Name
|
|
108
|
+
</Label>
|
|
109
|
+
<Input
|
|
110
|
+
id="name"
|
|
111
|
+
value={name}
|
|
112
|
+
onChange={(e) => setName(e.target.value)}
|
|
113
|
+
placeholder="e.g. Acme API"
|
|
114
|
+
required
|
|
115
|
+
disabled={isLoading}
|
|
116
|
+
className="bg-muted/30 border-muted"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className="grid gap-2">
|
|
121
|
+
<Label htmlFor="description" className="flex items-center gap-2">
|
|
122
|
+
<AlignLeft className="h-3.5 w-3.5 text-muted-foreground" />
|
|
123
|
+
Description <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
|
|
124
|
+
</Label>
|
|
125
|
+
<Textarea
|
|
126
|
+
id="description"
|
|
127
|
+
value={description}
|
|
128
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
129
|
+
className="resize-none min-h-[100px] bg-muted/30 border-muted"
|
|
130
|
+
disabled={isLoading}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div className="grid gap-2">
|
|
135
|
+
<Label htmlFor="color" className="flex items-center gap-2">
|
|
136
|
+
<Palette className="h-3.5 w-3.5 text-muted-foreground" />
|
|
137
|
+
Identity Color
|
|
138
|
+
</Label>
|
|
139
|
+
<div className="flex items-center gap-4">
|
|
140
|
+
<Input
|
|
141
|
+
id="color"
|
|
142
|
+
type="color"
|
|
143
|
+
value={color}
|
|
144
|
+
onChange={(e) => setColor(e.target.value)}
|
|
145
|
+
className="w-14 h-11 p-1 rounded-md border-muted cursor-pointer"
|
|
146
|
+
disabled={isLoading}
|
|
147
|
+
/>
|
|
148
|
+
<div className="flex-1 px-3 py-2 bg-muted/30 border border-muted rounded-md flex items-center justify-between">
|
|
149
|
+
<span className="text-sm font-mono uppercase font-semibold text-muted-foreground">
|
|
150
|
+
{color}
|
|
151
|
+
</span>
|
|
152
|
+
<div
|
|
153
|
+
className="w-4 h-4 rounded-full shadow-sm"
|
|
154
|
+
style={{ backgroundColor: color }}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<DialogFooter className="pt-2">
|
|
162
|
+
<Button
|
|
163
|
+
type="button"
|
|
164
|
+
variant="ghost"
|
|
165
|
+
onClick={() => setOpen(false)}
|
|
166
|
+
disabled={isLoading}
|
|
167
|
+
>
|
|
168
|
+
Cancel
|
|
169
|
+
</Button>
|
|
170
|
+
<Button type="submit" disabled={isLoading} className="px-8">
|
|
171
|
+
{isLoading ? "Updating..." : "Update Project"}
|
|
172
|
+
</Button>
|
|
173
|
+
</DialogFooter>
|
|
174
|
+
</form>
|
|
175
|
+
</DialogContent>
|
|
176
|
+
</Dialog>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
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, Eye, EyeOff, Database, Key, Type, AlignLeft, ShieldCheck, Calendar } from "lucide-react";
|
|
19
|
+
import { useVariables } from "@/hooks/use-variables";
|
|
20
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
21
|
+
|
|
22
|
+
interface EditVariableDialogProps {
|
|
23
|
+
variableId: string;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function EditVariableDialog({ variableId, children }: EditVariableDialogProps) {
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const [key, setKey] = useState("");
|
|
30
|
+
const [value, setValue] = useState("");
|
|
31
|
+
const [description, setDescription] = useState("");
|
|
32
|
+
const [isSecret, setIsSecret] = useState(true);
|
|
33
|
+
const [expiresAt, setExpiresAt] = useState("");
|
|
34
|
+
const [showValue, setShowValue] = useState(false);
|
|
35
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
36
|
+
const [error, setError] = useState("");
|
|
37
|
+
const { variables } = useVariables();
|
|
38
|
+
const queryClient = useQueryClient();
|
|
39
|
+
|
|
40
|
+
const variable = variables?.find(v => v.id === variableId);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (variable && open) {
|
|
44
|
+
setKey(variable.key);
|
|
45
|
+
setValue(variable.value);
|
|
46
|
+
setDescription(variable.description || "");
|
|
47
|
+
setIsSecret(variable.isSecret);
|
|
48
|
+
setExpiresAt(variable.expiresAt ? new Date(variable.expiresAt).toISOString().split('T')[0] : "");
|
|
49
|
+
setShowValue(false);
|
|
50
|
+
}
|
|
51
|
+
}, [variable, open]);
|
|
52
|
+
|
|
53
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
setError("");
|
|
56
|
+
setIsLoading(true);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch('/api/variables', {
|
|
60
|
+
method: 'PUT',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ id: variableId, key, value, description, isSecret, expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const data = await response.json();
|
|
68
|
+
|
|
69
|
+
if (response.ok) {
|
|
70
|
+
setOpen(false);
|
|
71
|
+
queryClient.invalidateQueries({ queryKey: ['variables'] });
|
|
72
|
+
} else {
|
|
73
|
+
setError(data.error || "Failed to update variable");
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
setError("An unexpected error occurred");
|
|
77
|
+
} finally {
|
|
78
|
+
setIsLoading(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
84
|
+
<DialogTrigger asChild>
|
|
85
|
+
{children || (
|
|
86
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
87
|
+
<Edit className="h-4 w-4" />
|
|
88
|
+
</Button>
|
|
89
|
+
)}
|
|
90
|
+
</DialogTrigger>
|
|
91
|
+
<DialogContent className="sm:max-w-[500px]">
|
|
92
|
+
<DialogHeader>
|
|
93
|
+
<div className="flex items-center gap-2 mb-1">
|
|
94
|
+
<div className="p-1.5 bg-primary/10 rounded-md">
|
|
95
|
+
<Database className="h-5 w-5 text-primary" />
|
|
96
|
+
</div>
|
|
97
|
+
<DialogTitle className="text-xl">Edit Variable</DialogTitle>
|
|
98
|
+
</div>
|
|
99
|
+
<DialogDescription>
|
|
100
|
+
Update the key, value, or security settings for this variable.
|
|
101
|
+
</DialogDescription>
|
|
102
|
+
</DialogHeader>
|
|
103
|
+
<form onSubmit={handleSubmit} className="space-y-6 pt-4">
|
|
104
|
+
{error && (
|
|
105
|
+
<div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
|
|
106
|
+
{error}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
<div className="space-y-4">
|
|
111
|
+
<div className="grid gap-2">
|
|
112
|
+
<Label htmlFor="key" className="flex items-center gap-2">
|
|
113
|
+
<Type className="h-3.5 w-3.5 text-muted-foreground" />
|
|
114
|
+
Variable Key
|
|
115
|
+
</Label>
|
|
116
|
+
<Input
|
|
117
|
+
id="key"
|
|
118
|
+
value={key}
|
|
119
|
+
onChange={(e) => setKey(e.target.value)}
|
|
120
|
+
placeholder="e.g. DATABASE_URL"
|
|
121
|
+
required
|
|
122
|
+
disabled={isLoading}
|
|
123
|
+
className="bg-muted/30 border-muted font-mono text-sm"
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="grid gap-2">
|
|
128
|
+
<div className="flex items-center justify-between">
|
|
129
|
+
<Label htmlFor="value" className="flex items-center gap-2">
|
|
130
|
+
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
|
131
|
+
Value
|
|
132
|
+
</Label>
|
|
133
|
+
{isSecret && (
|
|
134
|
+
<Button
|
|
135
|
+
type="button"
|
|
136
|
+
variant="ghost"
|
|
137
|
+
size="sm"
|
|
138
|
+
className="h-7 px-2 text-xs text-muted-foreground"
|
|
139
|
+
onClick={() => setShowValue(!showValue)}
|
|
140
|
+
disabled={isLoading}
|
|
141
|
+
>
|
|
142
|
+
{showValue ? <EyeOff className="h-3.5 w-3.5 mr-1.5" /> : <Eye className="h-3.5 w-3.5 mr-1.5" />}
|
|
143
|
+
{showValue ? "Hide" : "Show"}
|
|
144
|
+
</Button>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
<div className="relative">
|
|
148
|
+
<Input
|
|
149
|
+
id="value"
|
|
150
|
+
type={isSecret && !showValue ? "password" : "text"}
|
|
151
|
+
value={value}
|
|
152
|
+
onChange={(e) => setValue(e.target.value)}
|
|
153
|
+
placeholder="Enter variable value"
|
|
154
|
+
required
|
|
155
|
+
disabled={isLoading}
|
|
156
|
+
className="bg-muted/30 border-muted font-mono text-sm"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div className="grid gap-2">
|
|
162
|
+
<Label htmlFor="description" className="flex items-center gap-2">
|
|
163
|
+
<AlignLeft className="h-3.5 w-3.5 text-muted-foreground" />
|
|
164
|
+
Description <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
|
|
165
|
+
</Label>
|
|
166
|
+
<Input
|
|
167
|
+
id="description"
|
|
168
|
+
value={description}
|
|
169
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
170
|
+
placeholder="What is this variable for?"
|
|
171
|
+
disabled={isLoading}
|
|
172
|
+
className="bg-muted/30 border-muted"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className="grid gap-2">
|
|
177
|
+
<Label htmlFor="expiresAt" className="flex items-center gap-2">
|
|
178
|
+
<Calendar className="h-3.5 w-3.5 text-muted-foreground" />
|
|
179
|
+
Expiration Date <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
|
|
180
|
+
</Label>
|
|
181
|
+
<Input
|
|
182
|
+
id="expiresAt"
|
|
183
|
+
type="date"
|
|
184
|
+
value={expiresAt}
|
|
185
|
+
onChange={(e) => setExpiresAt(e.target.value)}
|
|
186
|
+
disabled={isLoading}
|
|
187
|
+
className="bg-muted/30 border-muted"
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div className="flex items-center space-x-3 p-3 bg-primary/5 rounded-lg border border-primary/10">
|
|
192
|
+
<Checkbox
|
|
193
|
+
id="isSecret"
|
|
194
|
+
checked={isSecret}
|
|
195
|
+
onCheckedChange={(checked) => setIsSecret(!!checked)}
|
|
196
|
+
disabled={isLoading}
|
|
197
|
+
className="data-[state=checked]:bg-primary"
|
|
198
|
+
/>
|
|
199
|
+
<div className="grid gap-1.5 leading-none">
|
|
200
|
+
<Label
|
|
201
|
+
htmlFor="isSecret"
|
|
202
|
+
className="text-sm font-medium leading-none cursor-pointer flex items-center gap-2"
|
|
203
|
+
>
|
|
204
|
+
<ShieldCheck className="h-3.5 w-3.5 text-primary" />
|
|
205
|
+
Encrypt as Secret
|
|
206
|
+
</Label>
|
|
207
|
+
<p className="text-xs text-muted-foreground">
|
|
208
|
+
Secrets are encrypted at rest and masked in the UI by default.
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<DialogFooter className="pt-2">
|
|
215
|
+
<Button
|
|
216
|
+
type="button"
|
|
217
|
+
variant="ghost"
|
|
218
|
+
onClick={() => setOpen(false)}
|
|
219
|
+
disabled={isLoading}
|
|
220
|
+
>
|
|
221
|
+
Cancel
|
|
222
|
+
</Button>
|
|
223
|
+
<Button type="submit" disabled={isLoading} className="px-8 shadow-sm">
|
|
224
|
+
{isLoading ? "Updating..." : "Update Variable"}
|
|
225
|
+
</Button>
|
|
226
|
+
</DialogFooter>
|
|
227
|
+
</form>
|
|
228
|
+
</DialogContent>
|
|
229
|
+
</Dialog>
|
|
230
|
+
);
|
|
231
|
+
}
|