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,512 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Badge } from "@/components/ui/badge";
|
|
8
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
9
|
+
import {
|
|
10
|
+
Plus,
|
|
11
|
+
Eye,
|
|
12
|
+
EyeOff,
|
|
13
|
+
Settings,
|
|
14
|
+
Users,
|
|
15
|
+
Shield,
|
|
16
|
+
Database,
|
|
17
|
+
Search,
|
|
18
|
+
Download,
|
|
19
|
+
Upload,
|
|
20
|
+
MoreVertical,
|
|
21
|
+
CheckCircle2,
|
|
22
|
+
Lock,
|
|
23
|
+
Unlock,
|
|
24
|
+
Webhook
|
|
25
|
+
} from "lucide-react";
|
|
26
|
+
import { useAuth } from "@/hooks/use-auth";
|
|
27
|
+
import { useTeams } from "@/hooks/use-teams";
|
|
28
|
+
import { useEnvironments } from "@/hooks/use-environments";
|
|
29
|
+
import { useVariables } from "@/hooks/use-variables";
|
|
30
|
+
import { useProjectPermissions } from "@/hooks/use-project-permissions";
|
|
31
|
+
import AppLayout from "@/components/AppLayout";
|
|
32
|
+
import CreateEnvironmentDialog from "@/components/CreateEnvironmentDialog";
|
|
33
|
+
import CreateVariableDialog from "@/components/CreateVariableDialog";
|
|
34
|
+
import EditTeamDialog from "@/components/EditTeamDialog";
|
|
35
|
+
import DeleteTeamDialog from "@/components/DeleteTeamDialog";
|
|
36
|
+
import EditEnvironmentDialog from "@/components/EditEnvironmentDialog";
|
|
37
|
+
import DeleteEnvironmentDialog from "@/components/DeleteEnvironmentDialog";
|
|
38
|
+
import InviteMemberDialog from "@/components/InviteMemberDialog";
|
|
39
|
+
import TeamMembersManager from "@/components/TeamMembersManager";
|
|
40
|
+
import BulkActions from "@/components/BulkActions";
|
|
41
|
+
import ImportVariablesDialog from "@/components/ImportVariablesDialog";
|
|
42
|
+
import VariableHistoryDialog from "@/components/VariableHistoryDialog";
|
|
43
|
+
import EditVariableDialog from "@/components/EditVariableDialog";
|
|
44
|
+
import DeleteVariableDialog from "@/components/DeleteVariableDialog";
|
|
45
|
+
import LeaveProjectDialog from "@/components/LeaveProjectDialog";
|
|
46
|
+
|
|
47
|
+
import { WebhooksManager } from "@/components/WebhooksManager";
|
|
48
|
+
import { ProjectDiff } from "@/components/ProjectDiff";
|
|
49
|
+
|
|
50
|
+
interface ProjectDetailsClientProps {
|
|
51
|
+
teamId: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default function ProjectDetailsClient({ teamId }: ProjectDetailsClientProps) {
|
|
55
|
+
const { user, isLoading: authLoading } = useAuth();
|
|
56
|
+
const { teams, isLoading: teamsLoading } = useTeams();
|
|
57
|
+
const { environments, isLoading: environmentsLoading } = useEnvironments();
|
|
58
|
+
const { variables, isLoading: variablesLoading } = useVariables();
|
|
59
|
+
const { canManageProject, canManageSecrets, members } = useProjectPermissions(teamId);
|
|
60
|
+
const router = useRouter();
|
|
61
|
+
|
|
62
|
+
const [visibleValues, setVisibleValues] = useState<Set<string>>(new Set());
|
|
63
|
+
const [selectedVariables, setSelectedVariables] = useState<string[]>([]);
|
|
64
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
65
|
+
const [activeEnvId, setActiveEnvId] = useState<string | undefined>(undefined);
|
|
66
|
+
|
|
67
|
+
const team = teams?.find(t => t.id === teamId);
|
|
68
|
+
const teamEnvironments = environments?.filter(e => e.teamId === teamId) || [];
|
|
69
|
+
const teamVariables = variables?.filter(v => v.teamId === teamId) || [];
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!activeEnvId && teamEnvironments.length > 0) {
|
|
73
|
+
setActiveEnvId(teamEnvironments[0].id);
|
|
74
|
+
}
|
|
75
|
+
}, [teamEnvironments, activeEnvId]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!authLoading && !user) {
|
|
79
|
+
router.push("/auth/login");
|
|
80
|
+
}
|
|
81
|
+
}, [user, authLoading, router]);
|
|
82
|
+
|
|
83
|
+
const toggleValueVisibility = (variableId: string) => {
|
|
84
|
+
const newVisible = new Set(visibleValues);
|
|
85
|
+
if (newVisible.has(variableId)) {
|
|
86
|
+
newVisible.delete(variableId);
|
|
87
|
+
} else {
|
|
88
|
+
newVisible.add(variableId);
|
|
89
|
+
}
|
|
90
|
+
setVisibleValues(newVisible);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleSelectAll = (envId: string) => {
|
|
94
|
+
const envVars = teamVariables.filter(v => v.environmentId === envId).map(v => v.id);
|
|
95
|
+
const allSelected = envVars.every(id => selectedVariables.includes(id));
|
|
96
|
+
|
|
97
|
+
if (allSelected) {
|
|
98
|
+
setSelectedVariables(selectedVariables.filter(id => !envVars.includes(id)));
|
|
99
|
+
} else {
|
|
100
|
+
setSelectedVariables([...new Set([...selectedVariables, ...envVars])]);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (authLoading || teamsLoading || environmentsLoading || variablesLoading) {
|
|
105
|
+
return (
|
|
106
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
107
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!user || !team) {
|
|
113
|
+
return (
|
|
114
|
+
<AppLayout>
|
|
115
|
+
<div className="flex flex-col items-center justify-center py-20">
|
|
116
|
+
<Shield className="h-16 w-16 text-muted-foreground mb-4" />
|
|
117
|
+
<h1 className="text-2xl font-bold mb-2">Project Not Found</h1>
|
|
118
|
+
<p className="text-muted-foreground mb-6">The project you're looking for doesn't exist or you don't have access to it.</p>
|
|
119
|
+
<Button onClick={() => router.push("/dashboard")}>Return to Dashboard</Button>
|
|
120
|
+
</div>
|
|
121
|
+
</AppLayout>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const filteredVariables = teamVariables.filter(v =>
|
|
126
|
+
v.key.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
127
|
+
(v.description?.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<AppLayout>
|
|
132
|
+
<div className="space-y-8">
|
|
133
|
+
{/* Header Section */}
|
|
134
|
+
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
|
135
|
+
<div className="space-y-1">
|
|
136
|
+
<div className="flex items-center gap-3">
|
|
137
|
+
<div
|
|
138
|
+
className="w-4 h-4 rounded-full"
|
|
139
|
+
style={{ backgroundColor: team.color || "#3b82f6" }}
|
|
140
|
+
/>
|
|
141
|
+
<h1 className="text-3xl font-bold tracking-tight">{team.name}</h1>
|
|
142
|
+
<Badge variant="outline" className="ml-2 font-mono text-xs uppercase opacity-70">
|
|
143
|
+
Project ID: {team.id.slice(0, 8)}
|
|
144
|
+
</Badge>
|
|
145
|
+
</div>
|
|
146
|
+
<p className="text-muted-foreground text-lg">
|
|
147
|
+
{team.description || "Manage your environment variables across different deployment stages."}
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex items-center gap-2">
|
|
151
|
+
{canManageProject ? (
|
|
152
|
+
<>
|
|
153
|
+
<InviteMemberDialog teamId={team.id} />
|
|
154
|
+
<EditTeamDialog teamId={team.id} />
|
|
155
|
+
</>
|
|
156
|
+
) : (
|
|
157
|
+
members?.find(m => m.userId === user.id) && (
|
|
158
|
+
<LeaveProjectDialog
|
|
159
|
+
teamId={team.id}
|
|
160
|
+
teamName={team.name}
|
|
161
|
+
memberId={members.find(m => m.userId === user.id)!.id}
|
|
162
|
+
/>
|
|
163
|
+
)
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Info Cards */}
|
|
169
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
170
|
+
<Card className="bg-primary/5 border-primary/20">
|
|
171
|
+
<CardContent className="pt-6">
|
|
172
|
+
<div className="flex items-center justify-between">
|
|
173
|
+
<div>
|
|
174
|
+
<p className="text-sm font-medium text-muted-foreground">Environments</p>
|
|
175
|
+
<h3 className="text-2xl font-bold">{teamEnvironments.length}</h3>
|
|
176
|
+
</div>
|
|
177
|
+
<div className="p-2 bg-primary/10 rounded-full">
|
|
178
|
+
<Shield className="h-5 w-5 text-primary" />
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</CardContent>
|
|
182
|
+
</Card>
|
|
183
|
+
<Card className="bg-blue-500/5 border-blue-500/20">
|
|
184
|
+
<CardContent className="pt-6">
|
|
185
|
+
<div className="flex items-center justify-between">
|
|
186
|
+
<div>
|
|
187
|
+
<p className="text-sm font-medium text-muted-foreground">Variables</p>
|
|
188
|
+
<h3 className="text-2xl font-bold">{teamVariables.length}</h3>
|
|
189
|
+
</div>
|
|
190
|
+
<div className="p-2 bg-blue-500/10 rounded-full">
|
|
191
|
+
<Database className="h-5 w-5 text-blue-500" />
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</CardContent>
|
|
195
|
+
</Card>
|
|
196
|
+
<Card className="bg-amber-500/5 border-amber-500/20">
|
|
197
|
+
<CardContent className="pt-6">
|
|
198
|
+
<div className="flex items-center justify-between">
|
|
199
|
+
<div>
|
|
200
|
+
<p className="text-sm font-medium text-muted-foreground">Secrets</p>
|
|
201
|
+
<h3 className="text-2xl font-bold">{teamVariables.filter(v => v.isSecret).length}</h3>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="p-2 bg-amber-500/10 rounded-full">
|
|
204
|
+
<Lock className="h-5 w-5 text-amber-500" />
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</CardContent>
|
|
208
|
+
</Card>
|
|
209
|
+
<Card className="bg-emerald-500/5 border-emerald-500/20">
|
|
210
|
+
<CardContent className="pt-6">
|
|
211
|
+
<div className="flex items-center justify-between">
|
|
212
|
+
<div>
|
|
213
|
+
<p className="text-sm font-medium text-muted-foreground">Team Size</p>
|
|
214
|
+
<h3 className="text-2xl font-bold">{members.length}</h3>
|
|
215
|
+
</div>
|
|
216
|
+
<div className="p-2 bg-emerald-500/10 rounded-full">
|
|
217
|
+
<Users className="h-5 w-5 text-emerald-500" />
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</CardContent>
|
|
221
|
+
</Card>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{/* Main Tabs */}
|
|
225
|
+
<Tabs defaultValue="variables" className="space-y-6">
|
|
226
|
+
<TabsList className="bg-muted p-1 border">
|
|
227
|
+
<TabsTrigger value="variables" className="px-8 py-2">Variables</TabsTrigger>
|
|
228
|
+
<TabsTrigger value="members" className="px-8 py-2">Team Members</TabsTrigger>
|
|
229
|
+
<TabsTrigger value="webhooks" className="px-8 py-2">Webhooks</TabsTrigger>
|
|
230
|
+
<TabsTrigger value="diff" className="px-8 py-2">Comparison</TabsTrigger>
|
|
231
|
+
<TabsTrigger value="settings" className="px-8 py-2">Settings</TabsTrigger>
|
|
232
|
+
</TabsList>
|
|
233
|
+
|
|
234
|
+
<TabsContent value="variables" className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
235
|
+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
236
|
+
<div className="relative w-full md:w-96">
|
|
237
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
238
|
+
<input
|
|
239
|
+
type="text"
|
|
240
|
+
placeholder="Search variables..."
|
|
241
|
+
className="w-full bg-card border rounded-md pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all"
|
|
242
|
+
value={searchQuery}
|
|
243
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="flex items-center gap-2">
|
|
247
|
+
{canManageSecrets && (
|
|
248
|
+
<>
|
|
249
|
+
<ImportVariablesDialog teamId={team.id} />
|
|
250
|
+
<CreateEnvironmentDialog teamId={team.id} />
|
|
251
|
+
<CreateVariableDialog environmentId={activeEnvId}>
|
|
252
|
+
<Button className="bg-primary hover:bg-primary/90 shadow-sm border-0">
|
|
253
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
254
|
+
New Variable
|
|
255
|
+
</Button>
|
|
256
|
+
</CreateVariableDialog>
|
|
257
|
+
</>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{selectedVariables.length > 0 && (
|
|
263
|
+
<BulkActions
|
|
264
|
+
selectedVariables={selectedVariables}
|
|
265
|
+
onSelectionChange={setSelectedVariables}
|
|
266
|
+
onRefresh={() => {}}
|
|
267
|
+
/>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
{teamEnvironments.length > 0 ? (
|
|
271
|
+
<Tabs
|
|
272
|
+
value={activeEnvId}
|
|
273
|
+
onValueChange={setActiveEnvId}
|
|
274
|
+
className="w-full"
|
|
275
|
+
>
|
|
276
|
+
<div className="flex items-center gap-2 overflow-x-auto pb-2 scrollbar-hide">
|
|
277
|
+
<TabsList className="bg-transparent border-0 gap-2">
|
|
278
|
+
{teamEnvironments.map((env) => (
|
|
279
|
+
<TabsTrigger
|
|
280
|
+
key={env.id}
|
|
281
|
+
value={env.id}
|
|
282
|
+
className="rounded-full border data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:shadow-md transition-all px-4"
|
|
283
|
+
>
|
|
284
|
+
<div
|
|
285
|
+
className="w-2 h-2 rounded-full mr-2"
|
|
286
|
+
style={{ backgroundColor: env.color || "#3b82f6" }}
|
|
287
|
+
/>
|
|
288
|
+
{env.name}
|
|
289
|
+
</TabsTrigger>
|
|
290
|
+
))}
|
|
291
|
+
</TabsList>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{teamEnvironments.map((env) => {
|
|
295
|
+
const envVars = filteredVariables.filter(v => v.environmentId === env.id);
|
|
296
|
+
const isAllSelected = envVars.length > 0 && envVars.every(v => selectedVariables.includes(v.id));
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<TabsContent key={env.id} value={env.id} className="mt-4 outline-none">
|
|
300
|
+
<div className="border rounded-xl bg-card shadow-sm overflow-hidden">
|
|
301
|
+
<div className="flex items-center justify-between px-6 py-4 bg-muted/30 border-b">
|
|
302
|
+
<div className="flex items-center gap-4">
|
|
303
|
+
<div className="flex items-center gap-2">
|
|
304
|
+
<input
|
|
305
|
+
type="checkbox"
|
|
306
|
+
checked={isAllSelected}
|
|
307
|
+
onChange={() => handleSelectAll(env.id)}
|
|
308
|
+
className="h-4 w-4 rounded border-muted-foreground accent-primary cursor-pointer"
|
|
309
|
+
/>
|
|
310
|
+
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
311
|
+
{envVars.length} Variables
|
|
312
|
+
</span>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="h-4 w-px bg-border hidden md:block" />
|
|
315
|
+
<div className="hidden md:flex items-center gap-1 text-xs text-muted-foreground">
|
|
316
|
+
<span>Status: Sync'd</span>
|
|
317
|
+
<CheckCircle2 className="h-3 w-3 text-emerald-500" />
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div className="flex items-center gap-2">
|
|
321
|
+
<EditEnvironmentDialog environmentId={env.id} />
|
|
322
|
+
<DeleteEnvironmentDialog environmentId={env.id} environmentName={env.name} />
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div className="divide-y divide-border">
|
|
327
|
+
{envVars.length > 0 ? (
|
|
328
|
+
envVars.map((variable) => (
|
|
329
|
+
<div
|
|
330
|
+
key={variable.id}
|
|
331
|
+
className={`group flex flex-col md:flex-row md:items-center p-4 transition-colors hover:bg-muted/40 ${selectedVariables.includes(variable.id) ? 'bg-primary/5' : ''}`}
|
|
332
|
+
>
|
|
333
|
+
<div className="flex items-center gap-4 flex-1">
|
|
334
|
+
<input
|
|
335
|
+
type="checkbox"
|
|
336
|
+
checked={selectedVariables.includes(variable.id)}
|
|
337
|
+
onChange={(e) => {
|
|
338
|
+
if (e.target.checked) {
|
|
339
|
+
setSelectedVariables([...selectedVariables, variable.id]);
|
|
340
|
+
} else {
|
|
341
|
+
setSelectedVariables(selectedVariables.filter(id => id !== variable.id));
|
|
342
|
+
}
|
|
343
|
+
}}
|
|
344
|
+
className="h-4 w-4 rounded border-muted-foreground accent-primary cursor-pointer"
|
|
345
|
+
/>
|
|
346
|
+
<div className="flex flex-col min-w-0">
|
|
347
|
+
<div className="flex items-center gap-2">
|
|
348
|
+
<code className="text-[13px] font-bold text-foreground truncate max-w-sm">
|
|
349
|
+
{variable.key}
|
|
350
|
+
</code>
|
|
351
|
+
{variable.isSecret ? (
|
|
352
|
+
<Badge variant="outline" className="text-[10px] h-4 border-amber-500/30 text-amber-600 bg-amber-500/5">
|
|
353
|
+
<Lock className="h-2 w-2 mr-1" /> SECRET
|
|
354
|
+
</Badge>
|
|
355
|
+
) : (
|
|
356
|
+
<Badge variant="outline" className="text-[10px] h-4 border-emerald-500/30 text-emerald-600 bg-emerald-500/5">
|
|
357
|
+
<Unlock className="h-2 w-2 mr-1" /> PUBLIC
|
|
358
|
+
</Badge>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
<p className="text-xs text-muted-foreground truncate max-w-md mt-0.5">
|
|
362
|
+
{variable.description || "No description provided."}
|
|
363
|
+
</p>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<div className="flex flex-1 items-center mt-2 md:mt-0 gap-3">
|
|
368
|
+
<div className="flex-1 flex items-center gap-2 bg-muted/60 px-3 py-1.5 rounded-md border text-sm max-w-full md:max-w-xs overflow-hidden">
|
|
369
|
+
{variable.isSecret ? (
|
|
370
|
+
<>
|
|
371
|
+
{visibleValues.has(variable.id) ? (
|
|
372
|
+
<code className="font-mono text-xs truncate">
|
|
373
|
+
{variable.value}
|
|
374
|
+
</code>
|
|
375
|
+
) : (
|
|
376
|
+
<span className="text-muted-foreground tracking-widest text-[13px] pt-1">••••••••••••••••</span>
|
|
377
|
+
)}
|
|
378
|
+
<button
|
|
379
|
+
onClick={() => toggleValueVisibility(variable.id)}
|
|
380
|
+
className="ml-auto text-muted-foreground hover:text-foreground transition-colors"
|
|
381
|
+
>
|
|
382
|
+
{visibleValues.has(variable.id) ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
|
383
|
+
</button>
|
|
384
|
+
</>
|
|
385
|
+
) : (
|
|
386
|
+
<code className="font-mono text-xs truncate">
|
|
387
|
+
{variable.value}
|
|
388
|
+
</code>
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<div className="flex items-center gap-1 mt-4 md:mt-0 md:ml-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
394
|
+
<VariableHistoryDialog variableId={variable.id} variableKey={variable.key} />
|
|
395
|
+
{canManageSecrets && (
|
|
396
|
+
<>
|
|
397
|
+
<EditVariableDialog variableId={variable.id} />
|
|
398
|
+
<DeleteVariableDialog variableId={variable.id} variableKey={variable.key} />
|
|
399
|
+
</>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
))
|
|
404
|
+
) : (
|
|
405
|
+
<div className="flex flex-col items-center justify-center py-16 gap-3">
|
|
406
|
+
<div className="p-3 bg-muted rounded-full">
|
|
407
|
+
<Database className="h-6 w-6 text-muted-foreground" />
|
|
408
|
+
</div>
|
|
409
|
+
<p className="text-muted-foreground text-sm">No variables found in this environment.</p>
|
|
410
|
+
{canManageSecrets && (
|
|
411
|
+
<CreateVariableDialog environmentId={env.id}>
|
|
412
|
+
<Button variant="outline" size="sm">Add Variable</Button>
|
|
413
|
+
</CreateVariableDialog>
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</TabsContent>
|
|
420
|
+
);
|
|
421
|
+
})}
|
|
422
|
+
</Tabs>
|
|
423
|
+
) : (
|
|
424
|
+
<Card className="border-dashed">
|
|
425
|
+
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
|
426
|
+
<Shield className="h-12 w-12 text-muted-foreground/30 mb-4" />
|
|
427
|
+
<h3 className="text-xl font-semibold mb-2">Initialize your environments</h3>
|
|
428
|
+
<p className="text-muted-foreground max-w-md mb-6">
|
|
429
|
+
Projects need at least one environment (like Development or Production) to store variables.
|
|
430
|
+
</p>
|
|
431
|
+
<CreateEnvironmentDialog teamId={team.id}>
|
|
432
|
+
<Button>Create My First Environment</Button>
|
|
433
|
+
</CreateEnvironmentDialog>
|
|
434
|
+
</CardContent>
|
|
435
|
+
</Card>
|
|
436
|
+
)}
|
|
437
|
+
</TabsContent>
|
|
438
|
+
|
|
439
|
+
<TabsContent value="members" className="animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
440
|
+
<Card>
|
|
441
|
+
<CardHeader>
|
|
442
|
+
<div className="flex items-center justify-between">
|
|
443
|
+
<div>
|
|
444
|
+
<CardTitle>Team Collaboration</CardTitle>
|
|
445
|
+
<CardDescription>
|
|
446
|
+
Grant teammates access to view or manage this project's configuration.
|
|
447
|
+
</CardDescription>
|
|
448
|
+
</div>
|
|
449
|
+
{canManageProject && <InviteMemberDialog teamId={team.id} />}
|
|
450
|
+
</div>
|
|
451
|
+
</CardHeader>
|
|
452
|
+
<CardContent>
|
|
453
|
+
<TeamMembersManager teamId={team.id} />
|
|
454
|
+
</CardContent>
|
|
455
|
+
</Card>
|
|
456
|
+
</TabsContent>
|
|
457
|
+
|
|
458
|
+
<TabsContent value="webhooks" className="animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
459
|
+
<WebhooksManager teamId={team.id} />
|
|
460
|
+
</TabsContent>
|
|
461
|
+
|
|
462
|
+
<TabsContent value="diff" className="animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
463
|
+
<ProjectDiff environments={teamEnvironments} variables={teamVariables} />
|
|
464
|
+
</TabsContent>
|
|
465
|
+
|
|
466
|
+
<TabsContent value="settings" className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
467
|
+
<Card>
|
|
468
|
+
<CardHeader>
|
|
469
|
+
<CardTitle>Project Settings</CardTitle>
|
|
470
|
+
<CardDescription>Manage general configuration and metadata for {team.name}.</CardDescription>
|
|
471
|
+
</CardHeader>
|
|
472
|
+
<CardContent className="space-y-4">
|
|
473
|
+
<div className="grid gap-2">
|
|
474
|
+
<h4 className="text-sm font-medium">Project Name</h4>
|
|
475
|
+
<div className="p-3 border rounded-md bg-muted/20 font-medium">{team.name}</div>
|
|
476
|
+
</div>
|
|
477
|
+
<div className="grid gap-2">
|
|
478
|
+
<h4 className="text-sm font-medium">Description</h4>
|
|
479
|
+
<div className="p-3 border rounded-md bg-muted/20 text-muted-foreground italic h-20">
|
|
480
|
+
{team.description || "No description set."}
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
</CardContent>
|
|
484
|
+
</Card>
|
|
485
|
+
|
|
486
|
+
{canManageProject && (
|
|
487
|
+
<Card className="border-destructive/20 bg-destructive/5">
|
|
488
|
+
<CardHeader>
|
|
489
|
+
<CardTitle className="text-destructive flex items-center gap-2">
|
|
490
|
+
Danger Zone
|
|
491
|
+
</CardTitle>
|
|
492
|
+
<CardDescription>Actions here are permanent and cannot be undone.</CardDescription>
|
|
493
|
+
</CardHeader>
|
|
494
|
+
<CardContent>
|
|
495
|
+
<div className="flex items-center justify-between p-4 border border-destructive/30 rounded-lg bg-card shadow-sm">
|
|
496
|
+
<div>
|
|
497
|
+
<h4 className="font-semibold text-destructive">Delete this project</h4>
|
|
498
|
+
<p className="text-sm text-muted-foreground mt-1 text-balance">
|
|
499
|
+
Permanently remove all environments, variables, and history for this project.
|
|
500
|
+
</p>
|
|
501
|
+
</div>
|
|
502
|
+
<DeleteTeamDialog teamId={team.id} teamName={team.name} />
|
|
503
|
+
</div>
|
|
504
|
+
</CardContent>
|
|
505
|
+
</Card>
|
|
506
|
+
)}
|
|
507
|
+
</TabsContent>
|
|
508
|
+
</Tabs>
|
|
509
|
+
</div>
|
|
510
|
+
</AppLayout>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Suspense } from 'react';
|
|
2
|
+
import ProjectDetailsClient from './ProjectDetailsClient';
|
|
3
|
+
|
|
4
|
+
interface PageProps {
|
|
5
|
+
params: Promise<{
|
|
6
|
+
id: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default async function ProjectDetailsPage({ params }: PageProps) {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
return (
|
|
13
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
14
|
+
<ProjectDetailsClient teamId={id} />
|
|
15
|
+
</Suspense>
|
|
16
|
+
);
|
|
17
|
+
}
|