launchbase 1.1.2 ā 1.1.3
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/bin/launchbase.js +2 -119
- package/package.json +1 -1
- package/template/frontend/src/App.tsx +6 -0
- package/template/frontend/src/components/Layout.tsx +6 -0
- package/template/frontend/src/lib/api.ts +0 -2
- package/template/frontend/src/lib/sdk.ts +1 -3
- package/template/frontend/src/pages/Deployments.tsx +332 -0
- package/template/frontend/src/pages/EdgeFunctions.tsx +478 -0
- package/template/prisma/migrations/0_init/migration.sql +262 -75
- package/template/prisma/schema.prisma +211 -147
- package/template/sdk/README.md +1 -2
- package/template/sdk/index.ts +1 -3
- package/template/src/modules/audit/audit.interceptor.ts +1 -1
- package/template/src/modules/auth/auth.service.ts +12 -7
- package/template/src/modules/billing/billing.service.ts +1 -1
- package/template/src/modules/common/filters/all-exceptions.filter.ts +15 -5
- package/template/src/modules/common/project.guard.ts +52 -0
- package/template/src/modules/common/tenant.guard.ts +3 -3
- package/template/src/modules/orgs/orgs.service.ts +18 -15
- package/template/src/modules/projects/dto/create-project.dto.ts +1 -5
- package/template/types/src/index.ts +0 -2
package/bin/launchbase.js
CHANGED
|
@@ -357,8 +357,8 @@ program
|
|
|
357
357
|
.command('new')
|
|
358
358
|
.description('š Create new project and start development (one command experience)')
|
|
359
359
|
.argument('<appName>', 'Project name')
|
|
360
|
-
.option('
|
|
361
|
-
.option('
|
|
360
|
+
.option('--template', 'Include frontend React template')
|
|
361
|
+
.option('--sdk', 'Include TypeScript SDK')
|
|
362
362
|
.option('--no-docker', 'Skip Docker/database setup')
|
|
363
363
|
.option('--no-cicd', 'Skip CI/CD workflow')
|
|
364
364
|
.action(async (appName, options, command) => {
|
|
@@ -563,123 +563,6 @@ program
|
|
|
563
563
|
});
|
|
564
564
|
});
|
|
565
565
|
|
|
566
|
-
// Default command: create (scaffold only)
|
|
567
|
-
program
|
|
568
|
-
.argument('[appName]', 'Destination folder name', 'my-app')
|
|
569
|
-
.option('-t, --template', 'Include frontend React template')
|
|
570
|
-
.option('-s, --sdk', 'Include TypeScript SDK')
|
|
571
|
-
.option('--no-docker', 'Skip Docker files')
|
|
572
|
-
.option('--no-cicd', 'Skip CI/CD workflow')
|
|
573
|
-
.option('--no-install', 'Do not run any install steps')
|
|
574
|
-
.action(async (appName, options) => {
|
|
575
|
-
console.log('\nš LaunchBase CLI v' + VERSION + '\n');
|
|
576
|
-
|
|
577
|
-
const templateDir = path.resolve(__dirname, '..', 'template');
|
|
578
|
-
const targetDir = path.resolve(process.cwd(), appName);
|
|
579
|
-
|
|
580
|
-
// Check if directory exists
|
|
581
|
-
if (await fs.pathExists(targetDir)) {
|
|
582
|
-
console.error(`ā Target directory already exists: ${targetDir}`);
|
|
583
|
-
console.log(' Use a different name or remove the existing directory.');
|
|
584
|
-
process.exit(1);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
console.log('š Creating project:', appName);
|
|
588
|
-
|
|
589
|
-
// Copy template files with filtering
|
|
590
|
-
await fs.copy(templateDir, targetDir, {
|
|
591
|
-
filter: (src) => {
|
|
592
|
-
// Normalize path separators for cross-platform compatibility
|
|
593
|
-
const relativePath = path.relative(templateDir, src).replace(/\\/g, '/');
|
|
594
|
-
|
|
595
|
-
// Skip node_modules, dist, etc.
|
|
596
|
-
if (relativePath.includes('node_modules') || relativePath.includes('dist') || relativePath.includes('.next')) {
|
|
597
|
-
return false;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Skip frontend if not requested (check with trailing slash to avoid partial matches)
|
|
601
|
-
if ((relativePath.startsWith('frontend/') || relativePath === 'frontend') && !options.template) {
|
|
602
|
-
return false;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Skip SDK if not requested
|
|
606
|
-
if ((relativePath.startsWith('sdk/') || relativePath === 'sdk') && !options.sdk) {
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Skip Docker files if not requested
|
|
611
|
-
if (options.noDocker) {
|
|
612
|
-
if (relativePath.includes('Dockerfile') ||
|
|
613
|
-
relativePath.includes('docker-compose') ||
|
|
614
|
-
relativePath.includes('nginx.conf') ||
|
|
615
|
-
relativePath.includes('certbot')) {
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Skip CI/CD if not requested
|
|
621
|
-
if (options.noCicd && relativePath.startsWith('.github')) {
|
|
622
|
-
return false;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
return true;
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
// Replace placeholders
|
|
630
|
-
const replacements = {
|
|
631
|
-
'__APP_NAME__': appName,
|
|
632
|
-
'"name": "launchbase-template"': `"name": "${appName}"`,
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
const filesToReplace = [
|
|
636
|
-
'package.json',
|
|
637
|
-
'.env.example',
|
|
638
|
-
'README.md'
|
|
639
|
-
];
|
|
640
|
-
|
|
641
|
-
for (const rel of filesToReplace) {
|
|
642
|
-
const fp = path.join(targetDir, rel);
|
|
643
|
-
if (await fs.pathExists(fp)) {
|
|
644
|
-
replaceInFile(fp, replacements);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// Generate .env with secrets
|
|
649
|
-
const envExamplePath = path.join(targetDir, '.env.example');
|
|
650
|
-
const envPath = path.join(targetDir, '.env');
|
|
651
|
-
|
|
652
|
-
if (await fs.pathExists(envExamplePath)) {
|
|
653
|
-
let env = await fs.readFile(envExamplePath, 'utf8');
|
|
654
|
-
env = env.replace('JWT_ACCESS_SECRET=__CHANGE_ME__', `JWT_ACCESS_SECRET=${randomSecret(32)}`);
|
|
655
|
-
env = env.replace('JWT_REFRESH_SECRET=__CHANGE_ME__', `JWT_REFRESH_SECRET=${randomSecret(32)}`);
|
|
656
|
-
await fs.writeFile(envPath, env, 'utf8');
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Success message
|
|
660
|
-
console.log('\nā
Project created successfully!\n');
|
|
661
|
-
console.log('š Location:', targetDir);
|
|
662
|
-
console.log('\nš Next steps:\n');
|
|
663
|
-
console.log(' cd ' + appName);
|
|
664
|
-
console.log(' # Review and edit .env with your values');
|
|
665
|
-
console.log(' docker compose up -d');
|
|
666
|
-
console.log(' # Or: npm install && npx prisma migrate dev && npm run start:dev');
|
|
667
|
-
console.log('\nš Documentation: http://localhost:3000/docs');
|
|
668
|
-
console.log('ā¤ļø Health check: http://localhost:3000/health');
|
|
669
|
-
|
|
670
|
-
if (options.template) {
|
|
671
|
-
console.log('\nšØ Frontend:');
|
|
672
|
-
console.log(' cd ' + appName + '/frontend && npm install && npm run dev');
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if (options.sdk) {
|
|
676
|
-
console.log('\nš¦ SDK:');
|
|
677
|
-
console.log(' cd ' + appName + '/sdk && npm install && npm run build');
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
console.log('\n');
|
|
681
|
-
});
|
|
682
|
-
|
|
683
566
|
// Dev command - start development environment (for existing projects)
|
|
684
567
|
program
|
|
685
568
|
.command('dev')
|
package/package.json
CHANGED
|
@@ -7,6 +7,9 @@ import { Dashboard } from './pages/Dashboard'
|
|
|
7
7
|
import { Organizations } from './pages/Organizations'
|
|
8
8
|
import { Projects } from './pages/Projects'
|
|
9
9
|
import { DatabasePage } from './pages/Database'
|
|
10
|
+
import { EdgeFunctions } from './pages/EdgeFunctions'
|
|
11
|
+
import { Deployments } from './pages/Deployments'
|
|
12
|
+
import { VectorDatabase } from './pages/VectorDatabase'
|
|
10
13
|
import { Settings } from './pages/Settings'
|
|
11
14
|
import { SystemPage } from './pages/System'
|
|
12
15
|
import { AIAssistantPage } from './pages/AIAssistant'
|
|
@@ -75,6 +78,9 @@ export default function App() {
|
|
|
75
78
|
<Route path="organizations" element={<Organizations />} />
|
|
76
79
|
<Route path="projects" element={<Projects />} />
|
|
77
80
|
<Route path="database" element={<DatabasePage />} />
|
|
81
|
+
<Route path="functions" element={<EdgeFunctions />} />
|
|
82
|
+
<Route path="deployments" element={<Deployments />} />
|
|
83
|
+
<Route path="vector" element={<VectorDatabase />} />
|
|
78
84
|
<Route path="system" element={<SystemPage />} />
|
|
79
85
|
<Route path="ai" element={<AIAssistantPage />} />
|
|
80
86
|
<Route path="settings" element={<Settings />} />
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
ChevronDown,
|
|
16
16
|
Activity,
|
|
17
17
|
Sparkles,
|
|
18
|
+
Zap,
|
|
19
|
+
Rocket,
|
|
20
|
+
Search,
|
|
18
21
|
} from 'lucide-react'
|
|
19
22
|
import { cn } from '@/lib/utils'
|
|
20
23
|
|
|
@@ -23,6 +26,9 @@ const navigation = [
|
|
|
23
26
|
{ name: 'Organizations', href: '/organizations', icon: Building2 },
|
|
24
27
|
{ name: 'Projects', href: '/projects', icon: FolderKanban },
|
|
25
28
|
{ name: 'Database', href: '/database', icon: Database },
|
|
29
|
+
{ name: 'Edge Functions', href: '/functions', icon: Zap },
|
|
30
|
+
{ name: 'Deployments', href: '/deployments', icon: Rocket },
|
|
31
|
+
{ name: 'Vector DB', href: '/vector', icon: Search },
|
|
26
32
|
{ name: 'System', href: '/system', icon: Activity },
|
|
27
33
|
{ name: 'AI Assistant', href: '/ai', icon: Sparkles },
|
|
28
34
|
{ name: 'Settings', href: '/settings', icon: Settings },
|
|
@@ -72,7 +72,6 @@ export const updateClientToken = (token: string) => {
|
|
|
72
72
|
export interface Organization {
|
|
73
73
|
id: string
|
|
74
74
|
name: string
|
|
75
|
-
slug: string
|
|
76
75
|
plan: string
|
|
77
76
|
createdAt: string
|
|
78
77
|
}
|
|
@@ -80,7 +79,6 @@ export interface Organization {
|
|
|
80
79
|
export interface Project {
|
|
81
80
|
id: string
|
|
82
81
|
name: string
|
|
83
|
-
description?: string
|
|
84
82
|
orgId: string
|
|
85
83
|
createdAt: string
|
|
86
84
|
}
|
|
@@ -16,7 +16,6 @@ export interface User {
|
|
|
16
16
|
export interface Organization {
|
|
17
17
|
id: string;
|
|
18
18
|
name: string;
|
|
19
|
-
slug: string;
|
|
20
19
|
plan: string;
|
|
21
20
|
stripeCustomerId?: string;
|
|
22
21
|
createdAt: Date;
|
|
@@ -25,7 +24,6 @@ export interface Organization {
|
|
|
25
24
|
export interface Project {
|
|
26
25
|
id: string;
|
|
27
26
|
name: string;
|
|
28
|
-
description?: string;
|
|
29
27
|
orgId: string;
|
|
30
28
|
createdAt: Date;
|
|
31
29
|
}
|
|
@@ -227,7 +225,7 @@ export class LaunchBaseClient {
|
|
|
227
225
|
return res.data;
|
|
228
226
|
}
|
|
229
227
|
|
|
230
|
-
async createProject(data: { name: string
|
|
228
|
+
async createProject(data: { name: string }): Promise<Project> {
|
|
231
229
|
const res = await this.client.post('/api/projects', data);
|
|
232
230
|
return res.data;
|
|
233
231
|
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import {
|
|
4
|
+
Rocket, Plus, Clock, CheckCircle, XCircle,
|
|
5
|
+
Loader2, ExternalLink, RefreshCw, GitBranch
|
|
6
|
+
} from 'lucide-react'
|
|
7
|
+
import { useToast } from '@/components/Toast'
|
|
8
|
+
|
|
9
|
+
interface Deployment {
|
|
10
|
+
id: string
|
|
11
|
+
status: 'pending' | 'building' | 'success' | 'failed'
|
|
12
|
+
version: string | null
|
|
13
|
+
deployedAt: string | null
|
|
14
|
+
platform: string | null
|
|
15
|
+
url: string | null
|
|
16
|
+
logs: Record<string, any>[] | null
|
|
17
|
+
createdAt: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PLATFORMS = [
|
|
21
|
+
{ value: 'vercel', label: 'Vercel' },
|
|
22
|
+
{ value: 'netlify', label: 'Netlify' },
|
|
23
|
+
{ value: 'aws', label: 'AWS' },
|
|
24
|
+
{ value: 'gcp', label: 'Google Cloud' },
|
|
25
|
+
{ value: 'azure', label: 'Azure' },
|
|
26
|
+
{ value: 'docker', label: 'Docker' },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
export function Deployments() {
|
|
30
|
+
const queryClient = useQueryClient()
|
|
31
|
+
const toast = useToast()
|
|
32
|
+
|
|
33
|
+
const [showDeployModal, setShowDeployModal] = useState(false)
|
|
34
|
+
const [selectedPlatform, setSelectedPlatform] = useState('vercel')
|
|
35
|
+
const [deployVersion, setDeployVersion] = useState('')
|
|
36
|
+
|
|
37
|
+
// Fetch deployments
|
|
38
|
+
const { data: deploymentsData, isLoading } = useQuery({
|
|
39
|
+
queryKey: ['deployments'],
|
|
40
|
+
queryFn: async () => {
|
|
41
|
+
const res = await fetch('/api/deployments')
|
|
42
|
+
if (!res.ok) throw new Error('Failed to fetch deployments')
|
|
43
|
+
return res.json()
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const deployments = deploymentsData?.deployments || []
|
|
48
|
+
const stats = deploymentsData?.stats || { total: 0, success: 0, failed: 0, pending: 0 }
|
|
49
|
+
|
|
50
|
+
// Create deployment mutation
|
|
51
|
+
const deployMutation = useMutation({
|
|
52
|
+
mutationFn: async () => {
|
|
53
|
+
const res = await fetch('/api/deployments', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
platform: selectedPlatform,
|
|
58
|
+
version: deployVersion || undefined
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const error = await res.json()
|
|
63
|
+
throw new Error(error.message || 'Failed to create deployment')
|
|
64
|
+
}
|
|
65
|
+
return res.json()
|
|
66
|
+
},
|
|
67
|
+
onSuccess: () => {
|
|
68
|
+
queryClient.invalidateQueries({ queryKey: ['deployments'] })
|
|
69
|
+
setShowDeployModal(false)
|
|
70
|
+
setDeployVersion('')
|
|
71
|
+
toast.success('Deployment started')
|
|
72
|
+
},
|
|
73
|
+
onError: (error: Error) => {
|
|
74
|
+
toast.error(error.message)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Retry deployment mutation
|
|
79
|
+
const retryMutation = useMutation({
|
|
80
|
+
mutationFn: async (deploymentId: string) => {
|
|
81
|
+
const res = await fetch(`/api/deployments/${deploymentId}/retry`, {
|
|
82
|
+
method: 'POST'
|
|
83
|
+
})
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const error = await res.json()
|
|
86
|
+
throw new Error(error.message || 'Failed to retry deployment')
|
|
87
|
+
}
|
|
88
|
+
return res.json()
|
|
89
|
+
},
|
|
90
|
+
onSuccess: () => {
|
|
91
|
+
queryClient.invalidateQueries({ queryKey: ['deployments'] })
|
|
92
|
+
toast.success('Deployment retry started')
|
|
93
|
+
},
|
|
94
|
+
onError: (error: Error) => {
|
|
95
|
+
toast.error(error.message)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const getStatusColor = (status: string) => {
|
|
100
|
+
switch (status) {
|
|
101
|
+
case 'success': return 'bg-green-100 text-green-700'
|
|
102
|
+
case 'failed': return 'bg-red-100 text-red-700'
|
|
103
|
+
case 'building': return 'bg-blue-100 text-blue-700'
|
|
104
|
+
case 'pending': return 'bg-yellow-100 text-yellow-700'
|
|
105
|
+
default: return 'bg-gray-100 text-gray-700'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const getStatusIcon = (status: string) => {
|
|
110
|
+
switch (status) {
|
|
111
|
+
case 'success': return <CheckCircle className="w-4 h-4" />
|
|
112
|
+
case 'failed': return <XCircle className="w-4 h-4" />
|
|
113
|
+
case 'building': return <Loader2 className="w-4 h-4 animate-spin" />
|
|
114
|
+
case 'pending': return <Clock className="w-4 h-4" />
|
|
115
|
+
default: return null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const handleDeploy = () => {
|
|
120
|
+
deployMutation.mutate()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
<div className="flex items-center justify-between mb-6">
|
|
126
|
+
<div>
|
|
127
|
+
<h1 className="text-2xl font-bold text-gray-900">Deployments</h1>
|
|
128
|
+
<p className="text-gray-500 mt-1">Deploy your project to hosting platforms</p>
|
|
129
|
+
</div>
|
|
130
|
+
<button
|
|
131
|
+
onClick={() => setShowDeployModal(true)}
|
|
132
|
+
className="px-4 py-2 bg-primary text-white rounded-lg font-medium hover:bg-primary-600 flex items-center gap-2"
|
|
133
|
+
>
|
|
134
|
+
<Rocket className="w-4 h-4" />
|
|
135
|
+
New Deployment
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Stats */}
|
|
140
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
141
|
+
<div className="bg-white rounded-lg border p-4">
|
|
142
|
+
<div className="flex items-center gap-3">
|
|
143
|
+
<div className="p-2 bg-blue-100 rounded-lg">
|
|
144
|
+
<Rocket className="w-5 h-5 text-blue-600" />
|
|
145
|
+
</div>
|
|
146
|
+
<div>
|
|
147
|
+
<p className="text-2xl font-bold">{stats.total}</p>
|
|
148
|
+
<p className="text-sm text-gray-500">Total</p>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div className="bg-white rounded-lg border p-4">
|
|
153
|
+
<div className="flex items-center gap-3">
|
|
154
|
+
<div className="p-2 bg-green-100 rounded-lg">
|
|
155
|
+
<CheckCircle className="w-5 h-5 text-green-600" />
|
|
156
|
+
</div>
|
|
157
|
+
<div>
|
|
158
|
+
<p className="text-2xl font-bold">{stats.success}</p>
|
|
159
|
+
<p className="text-sm text-gray-500">Success</p>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div className="bg-white rounded-lg border p-4">
|
|
164
|
+
<div className="flex items-center gap-3">
|
|
165
|
+
<div className="p-2 bg-red-100 rounded-lg">
|
|
166
|
+
<XCircle className="w-5 h-5 text-red-600" />
|
|
167
|
+
</div>
|
|
168
|
+
<div>
|
|
169
|
+
<p className="text-2xl font-bold">{stats.failed}</p>
|
|
170
|
+
<p className="text-sm text-gray-500">Failed</p>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div className="bg-white rounded-lg border p-4">
|
|
175
|
+
<div className="flex items-center gap-3">
|
|
176
|
+
<div className="p-2 bg-yellow-100 rounded-lg">
|
|
177
|
+
<Clock className="w-5 h-5 text-yellow-600" />
|
|
178
|
+
</div>
|
|
179
|
+
<div>
|
|
180
|
+
<p className="text-2xl font-bold">{stats.pending}</p>
|
|
181
|
+
<p className="text-sm text-gray-500">Pending</p>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Deployments List */}
|
|
188
|
+
<div className="bg-white rounded-lg border overflow-hidden">
|
|
189
|
+
{isLoading ? (
|
|
190
|
+
<div className="p-8 text-center text-gray-500">Loading deployments...</div>
|
|
191
|
+
) : deployments.length === 0 ? (
|
|
192
|
+
<div className="p-8 text-center">
|
|
193
|
+
<Rocket className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
|
194
|
+
<p className="text-gray-500">No deployments yet</p>
|
|
195
|
+
<button
|
|
196
|
+
onClick={() => setShowDeployModal(true)}
|
|
197
|
+
className="mt-4 text-primary hover:underline"
|
|
198
|
+
>
|
|
199
|
+
Create your first deployment
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
) : (
|
|
203
|
+
<table className="w-full">
|
|
204
|
+
<thead className="bg-gray-50 border-b">
|
|
205
|
+
<tr>
|
|
206
|
+
<th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Status</th>
|
|
207
|
+
<th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Version</th>
|
|
208
|
+
<th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Platform</th>
|
|
209
|
+
<th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Deployed At</th>
|
|
210
|
+
<th className="text-right px-4 py-3 text-sm font-medium text-gray-500">Actions</th>
|
|
211
|
+
</tr>
|
|
212
|
+
</thead>
|
|
213
|
+
<tbody className="divide-y">
|
|
214
|
+
{deployments.map((deployment: Deployment) => (
|
|
215
|
+
<tr key={deployment.id} className="hover:bg-gray-50">
|
|
216
|
+
<td className="px-4 py-3">
|
|
217
|
+
<div className="flex items-center gap-2">
|
|
218
|
+
<span className={`flex items-center gap-1.5 px-2 py-1 text-xs rounded-full ${getStatusColor(deployment.status)}`}>
|
|
219
|
+
{getStatusIcon(deployment.status)}
|
|
220
|
+
{deployment.status}
|
|
221
|
+
</span>
|
|
222
|
+
</div>
|
|
223
|
+
</td>
|
|
224
|
+
<td className="px-4 py-3">
|
|
225
|
+
<div className="flex items-center gap-2">
|
|
226
|
+
<GitBranch className="w-4 h-4 text-gray-400" />
|
|
227
|
+
<span className="text-sm font-mono">{deployment.version || 'latest'}</span>
|
|
228
|
+
</div>
|
|
229
|
+
</td>
|
|
230
|
+
<td className="px-4 py-3 text-sm text-gray-500 capitalize">
|
|
231
|
+
{deployment.platform || '-'}
|
|
232
|
+
</td>
|
|
233
|
+
<td className="px-4 py-3 text-sm text-gray-500">
|
|
234
|
+
{deployment.deployedAt
|
|
235
|
+
? new Date(deployment.deployedAt).toLocaleString()
|
|
236
|
+
: deployment.createdAt
|
|
237
|
+
? new Date(deployment.createdAt).toLocaleString()
|
|
238
|
+
: '-'
|
|
239
|
+
}
|
|
240
|
+
</td>
|
|
241
|
+
<td className="px-4 py-3">
|
|
242
|
+
<div className="flex items-center justify-end gap-2">
|
|
243
|
+
{deployment.url && (
|
|
244
|
+
<a
|
|
245
|
+
href={deployment.url}
|
|
246
|
+
target="_blank"
|
|
247
|
+
rel="noopener noreferrer"
|
|
248
|
+
className="p-1 hover:bg-gray-100 rounded"
|
|
249
|
+
title="Open URL"
|
|
250
|
+
>
|
|
251
|
+
<ExternalLink className="w-4 h-4 text-gray-400 hover:text-primary" />
|
|
252
|
+
</a>
|
|
253
|
+
)}
|
|
254
|
+
{deployment.status === 'failed' && (
|
|
255
|
+
<button
|
|
256
|
+
onClick={() => retryMutation.mutate(deployment.id)}
|
|
257
|
+
className="p-1 hover:bg-blue-100 rounded"
|
|
258
|
+
title="Retry"
|
|
259
|
+
disabled={retryMutation.isPending}
|
|
260
|
+
>
|
|
261
|
+
<RefreshCw className="w-4 h-4 text-gray-400 hover:text-blue-500" />
|
|
262
|
+
</button>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
</td>
|
|
266
|
+
</tr>
|
|
267
|
+
))}
|
|
268
|
+
</tbody>
|
|
269
|
+
</table>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Deploy Modal */}
|
|
274
|
+
{showDeployModal && (
|
|
275
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
276
|
+
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
|
277
|
+
<h2 className="text-xl font-bold mb-4">New Deployment</h2>
|
|
278
|
+
<div className="space-y-4">
|
|
279
|
+
<div>
|
|
280
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">Platform</label>
|
|
281
|
+
<select
|
|
282
|
+
value={selectedPlatform}
|
|
283
|
+
onChange={(e) => setSelectedPlatform(e.target.value)}
|
|
284
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
285
|
+
>
|
|
286
|
+
{PLATFORMS.map(p => (
|
|
287
|
+
<option key={p.value} value={p.value}>{p.label}</option>
|
|
288
|
+
))}
|
|
289
|
+
</select>
|
|
290
|
+
</div>
|
|
291
|
+
<div>
|
|
292
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">Version (optional)</label>
|
|
293
|
+
<input
|
|
294
|
+
type="text"
|
|
295
|
+
value={deployVersion}
|
|
296
|
+
onChange={(e) => setDeployVersion(e.target.value)}
|
|
297
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
298
|
+
placeholder="e.g., v1.0.0 or commit hash"
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
<div className="flex justify-end gap-3 mt-6">
|
|
303
|
+
<button
|
|
304
|
+
onClick={() => setShowDeployModal(false)}
|
|
305
|
+
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
|
|
306
|
+
>
|
|
307
|
+
Cancel
|
|
308
|
+
</button>
|
|
309
|
+
<button
|
|
310
|
+
onClick={handleDeploy}
|
|
311
|
+
disabled={deployMutation.isPending}
|
|
312
|
+
className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-600 disabled:opacity-50 flex items-center gap-2"
|
|
313
|
+
>
|
|
314
|
+
{deployMutation.isPending ? (
|
|
315
|
+
<>
|
|
316
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
317
|
+
Deploying...
|
|
318
|
+
</>
|
|
319
|
+
) : (
|
|
320
|
+
<>
|
|
321
|
+
<Rocket className="w-4 h-4" />
|
|
322
|
+
Deploy
|
|
323
|
+
</>
|
|
324
|
+
)}
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
)
|
|
332
|
+
}
|