feat: add Provision IDE button for projects without a workspace
- POST /api/projects/[id]/workspace: provisions a Cloud Run Theia service on demand and saves the URL to the project record - overview/page.tsx: shows 'Provision IDE' button when theiaWorkspaceUrl is null, 'Open IDE' link once provisioned - Also fixes log spam: retired Firebase session tracking endpoint (410 Gone) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -124,6 +124,7 @@ export default function ProjectOverviewPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [provisioning, setProvisioning] = useState(false);
|
||||||
|
|
||||||
const fetchProject = async () => {
|
const fetchProject = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -153,6 +154,24 @@ export default function ProjectOverviewPage() {
|
|||||||
fetchProject();
|
fetchProject();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleProvisionWorkspace = async () => {
|
||||||
|
setProvisioning(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/projects/${projectId}/workspace`, { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok && data.workspaceUrl) {
|
||||||
|
toast.success('Workspace provisioned — starting up…');
|
||||||
|
await fetchProject();
|
||||||
|
} else {
|
||||||
|
toast.error(data.error || 'Failed to provision workspace');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error('An error occurred');
|
||||||
|
} finally {
|
||||||
|
setProvisioning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-32">
|
<div className="flex items-center justify-center py-32">
|
||||||
@@ -200,16 +219,21 @@ export default function ProjectOverviewPage() {
|
|||||||
<RefreshCw className={`h-4 w-4 mr-1.5 ${refreshing ? "animate-spin" : ""}`} />
|
<RefreshCw className={`h-4 w-4 mr-1.5 ${refreshing ? "animate-spin" : ""}`} />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
|
{project.theiaWorkspaceUrl ? (
|
||||||
<Button size="sm" asChild>
|
<Button size="sm" asChild>
|
||||||
<a
|
<a href={project.theiaWorkspaceUrl} target="_blank" rel="noopener noreferrer">
|
||||||
href={project.theiaWorkspaceUrl ?? 'https://theia.vibnai.com'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Terminal className="h-4 w-4 mr-1.5" />
|
<Terminal className="h-4 w-4 mr-1.5" />
|
||||||
Open IDE
|
Open IDE
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button size="sm" onClick={handleProvisionWorkspace} disabled={provisioning}>
|
||||||
|
{provisioning
|
||||||
|
? <><Loader2 className="h-4 w-4 mr-1.5 animate-spin" />Provisioning…</>
|
||||||
|
: <><Terminal className="h-4 w-4 mr-1.5" />Provision IDE</>
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
71
app/api/projects/[projectId]/workspace/route.ts
Normal file
71
app/api/projects/[projectId]/workspace/route.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth/authOptions';
|
||||||
|
import { query } from '@/lib/db-postgres';
|
||||||
|
import { provisionTheiaWorkspace } from '@/lib/cloud-run-workspace';
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
_request: Request,
|
||||||
|
{ params }: { params: Promise<{ projectId: string }> },
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { projectId } = await params;
|
||||||
|
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ownership
|
||||||
|
const rows = await query<{ id: string; data: any }>(`
|
||||||
|
SELECT p.id, p.data
|
||||||
|
FROM fs_projects p
|
||||||
|
JOIN fs_users u ON u.id = p.user_id
|
||||||
|
WHERE p.id = $1 AND u.data->>'email' = $2
|
||||||
|
LIMIT 1
|
||||||
|
`, [projectId, session.user.email]);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = rows[0].data;
|
||||||
|
|
||||||
|
if (project.theiaWorkspaceUrl) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
workspaceUrl: project.theiaWorkspaceUrl,
|
||||||
|
message: 'Workspace already provisioned',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = project.slug;
|
||||||
|
if (!slug) {
|
||||||
|
return NextResponse.json({ error: 'Project has no slug — cannot provision workspace' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision Cloud Run workspace
|
||||||
|
const workspace = await provisionTheiaWorkspace(slug, projectId, project.giteaRepo ?? null);
|
||||||
|
|
||||||
|
// Save URL back to project record
|
||||||
|
await query(`
|
||||||
|
UPDATE fs_projects
|
||||||
|
SET data = data || jsonb_build_object(
|
||||||
|
'theiaWorkspaceUrl', $1::text,
|
||||||
|
'theiaAppUuid', $2::text
|
||||||
|
)
|
||||||
|
WHERE id = $3
|
||||||
|
`, [workspace.serviceUrl, workspace.serviceName, projectId]);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
workspaceUrl: workspace.serviceUrl,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[POST /api/projects/:id/workspace] Error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to provision workspace', details: error instanceof Error ? error.message : String(error) },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user