From 9a65a54de565c213aea9f0dea33c40bbc046481d Mon Sep 17 00:00:00 2001 From: NarwhalChen <125920907+NarwhalChen@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:39:41 -0600 Subject: [PATCH] feat(frontend): Code Engine preview, support preview project right now (#121) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- backend/database.db | Bin 114688 -> 139264 bytes backend/src/chat/chat.model.ts | 4 - backend/src/project/project.model.ts | 4 +- backend/template/react-ts/Dockerfile | 15 +++ backend/template/react-ts/vite.config.ts | 14 ++ docker-compose.yml | 25 ++++ frontend/src/app/api/runProject/route.ts | 127 ++++++++++++++++++ .../components/code-engine/code-engine.tsx | 3 +- .../code-engine/project-context.tsx | 4 +- .../src/components/code-engine/web-view.tsx | 84 ++++++++++++ frontend/src/graphql/schema.gql | 4 +- 11 files changed, 275 insertions(+), 9 deletions(-) create mode 100644 backend/template/react-ts/Dockerfile create mode 100644 docker-compose.yml create mode 100644 frontend/src/app/api/runProject/route.ts create mode 100644 frontend/src/components/code-engine/web-view.tsx diff --git a/backend/database.db b/backend/database.db index 4e03bad6efcd39bcf5925dffaa848096d686c99c..030f20563a79d84d25552da743e9aaf12e0bc65f 100644 GIT binary patch literal 139264 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 diff --git a/backend/src/chat/chat.model.ts b/backend/src/chat/chat.model.ts index f782dce2..ef94c81f 100644 --- a/backend/src/chat/chat.model.ts +++ b/backend/src/chat/chat.model.ts @@ -48,10 +48,6 @@ export class Chat extends SystemBaseModel { }) messages: Message[]; - @Field(() => ID) - @Column() - projectId: string; - @ManyToOne(() => User, (user) => user.chats, { onDelete: 'CASCADE', nullable: false, diff --git a/backend/src/project/project.model.ts b/backend/src/project/project.model.ts index f4937025..83b585ff 100644 --- a/backend/src/project/project.model.ts +++ b/backend/src/project/project.model.ts @@ -9,6 +9,7 @@ import { ManyToMany, JoinTable, OneToMany, + RelationId, } from 'typeorm'; import { User } from 'src/user/user.model'; import { ProjectPackages } from './project-packages.model'; @@ -30,7 +31,8 @@ export class Project extends SystemBaseModel { projectPath: string; @Field(() => ID) - @Column() + @RelationId((project: Project) => project.user) + @Column({ name: 'user_id' }) userId: string; @ManyToOne(() => User, (user) => user.projects, { diff --git a/backend/template/react-ts/Dockerfile b/backend/template/react-ts/Dockerfile new file mode 100644 index 00000000..76508c58 --- /dev/null +++ b/backend/template/react-ts/Dockerfile @@ -0,0 +1,15 @@ +FROM node:18 + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install --frozen-lockfile + +COPY . . + +RUN chmod +x /app/node_modules/.bin/vite + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/backend/template/react-ts/vite.config.ts b/backend/template/react-ts/vite.config.ts index 09bfddc5..97ca8705 100644 --- a/backend/template/react-ts/vite.config.ts +++ b/backend/template/react-ts/vite.config.ts @@ -15,5 +15,19 @@ export default defineConfig({ }, build: { target: 'esnext', // Ensure Vite compiles for a modern target + sourcemap: false, + rollupOptions: { + output: { + manualChunks: undefined, // avoid sending code by chunk + }, + }, + }, + server: { + host: '0.0.0.0', + port: 5173, + strictPort: true, + watch: { + usePolling: true, + }, }, }); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..8b3ff438 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3' + +services: + reverse-proxy: + # The official v3 Traefik docker image + image: traefik:v3.3 + # Enables the web UI and tells Traefik to listen to docker + command: + - '--api.insecure=true' + - '--providers.docker' + - '--entrypoints.web.address=:80' + ports: + # The HTTP port + - '80:80' + # The Web UI (enabled by --api.insecure=true) + - '9001:8080' + volumes: + # So that Traefik can listen to the Docker events + - /var/run/docker.sock:/var/run/docker.sock + networks: + - traefik_network + +networks: + traefik_network: + external: true diff --git a/frontend/src/app/api/runProject/route.ts b/frontend/src/app/api/runProject/route.ts new file mode 100644 index 00000000..8be2fb36 --- /dev/null +++ b/frontend/src/app/api/runProject/route.ts @@ -0,0 +1,127 @@ +import { NextResponse } from 'next/server'; +import { exec } from 'child_process'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import * as net from 'net'; +import { getProjectPath } from 'codefox-common'; + +const runningContainers = new Map< + string, + { domain: string; containerId: string } +>(); +const allocatedPorts = new Set(); + +function findAvailablePort( + minPort: number = 38000, + maxPort: number = 42000 +): Promise { + return new Promise((resolve, reject) => { + function checkPort(port: number): Promise { + return new Promise((resolve) => { + if (allocatedPorts.has(port)) { + return resolve(false); + } + const server = net.createServer(); + server.listen(port, '127.0.0.1', () => { + server.close(() => resolve(true)); + }); + server.on('error', () => resolve(false)); + }); + } + + async function scanPorts() { + for (let port = minPort; port <= maxPort; port++) { + if (await checkPort(port)) { + allocatedPorts.add(port); + return resolve(port); + } + } + reject(new Error('No available ports found.')); + } + + scanPorts(); + }); +} + +async function buildAndRunDocker( + projectPath: string +): Promise<{ domain: string; containerId: string }> { + console.log(runningContainers); + if (runningContainers.has(projectPath)) { + console.log(`Container for project ${projectPath} is already running.`); + return runningContainers.get(projectPath)!; + } + const traefikDomain = process.env.TRAEFIK_DOMAIN || 'docker.localhost'; + const directory = path.join(getProjectPath(projectPath), 'frontend'); + const imageName = projectPath.toLowerCase(); + const containerId = crypto.randomUUID(); + const containerName = `container-${containerId}`; + + const subdomain = projectPath.replace(/[^\w-]/g, '').toLowerCase(); + const domain = `${subdomain}.${traefikDomain}`; + const exposedPort = await findAvailablePort(); + return new Promise((resolve, reject) => { + exec( + `docker build -t ${imageName} ${directory}`, + (buildErr, buildStdout, buildStderr) => { + if (buildErr) { + console.error(`Error during Docker build: ${buildStderr}`); + return reject(buildErr); + } + + console.log(`Docker build output:\n${buildStdout}`); + console.log(`Running Docker container: ${containerName}`); + + const runCommand = `docker run -d --name ${containerName} -l "traefik.enable=true" \ + -l "traefik.http.routers.${subdomain}.rule=Host(\\"${domain}\\")" \ + -l "traefik.http.services.${subdomain}.loadbalancer.server.port=5173" \ + --network=traefik_network -p ${exposedPort}:5173 ${imageName}`; + console.log(runCommand); + + exec(runCommand, (runErr, runStdout, runStderr) => { + if (runErr) { + console.error(`Error during Docker run: ${runStderr}`); + return reject(runErr); + } + + const containerActualId = runStdout.trim(); + runningContainers.set(projectPath, { + domain, + containerId: containerActualId, + }); + + console.log( + `Container ${containerName} is now running at http://${domain}` + ); + + resolve({ domain, containerId: containerActualId }); + }); + } + ); + }); +} +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const projectPath = searchParams.get('projectPath'); + + if (!projectPath) { + return NextResponse.json( + { error: 'Missing required parameters' }, + { status: 400 } + ); + } + + try { + const { domain, containerId } = await buildAndRunDocker(projectPath); + return NextResponse.json({ + message: 'Docker container started', + domain, + containerId, + }); + } catch (error) { + return NextResponse.json( + { error: error.message || 'Failed to start Docker container' }, + { status: 500 } + ); + } +} diff --git a/frontend/src/components/code-engine/code-engine.tsx b/frontend/src/components/code-engine/code-engine.tsx index b7a28733..451fda37 100644 --- a/frontend/src/components/code-engine/code-engine.tsx +++ b/frontend/src/components/code-engine/code-engine.tsx @@ -18,6 +18,7 @@ import { TreeItem, TreeItemIndex } from 'react-complex-tree'; import FileExplorerButton from './file-explorer-button'; import FileStructure from './file-structure'; import { ProjectContext } from './project-context'; +import WebPreview from './web-view'; export function CodeEngine({ chatId }: { chatId: string }) { // Initialize state, refs, and context @@ -314,7 +315,7 @@ export function CodeEngine({ chatId }: { chatId: string }) { ) : activeTab === 'preview' ? ( -
Preview Content (Mock)
+ ) : activeTab === 'console' ? (
Console Content (Mock)
) : null} diff --git a/frontend/src/components/code-engine/project-context.tsx b/frontend/src/components/code-engine/project-context.tsx index 70101578..44cc81f5 100644 --- a/frontend/src/components/code-engine/project-context.tsx +++ b/frontend/src/components/code-engine/project-context.tsx @@ -34,7 +34,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { const [chatProjectCache, setChatProjectCache] = useState< Map >(new Map()); - const MAX_RETRIES = 20; + const MAX_RETRIES = 100; useQuery(GET_USER_PROJECTS, { onCompleted: (data) => setProjects(data.getUserProjects), @@ -106,7 +106,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { console.error('Error polling chat:', error); } - await new Promise((resolve) => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 6000)); retries++; } diff --git a/frontend/src/components/code-engine/web-view.tsx b/frontend/src/components/code-engine/web-view.tsx new file mode 100644 index 00000000..b27d7eac --- /dev/null +++ b/frontend/src/components/code-engine/web-view.tsx @@ -0,0 +1,84 @@ +import { useContext, useEffect, useRef, useState } from 'react'; +import { ProjectContext } from './project-context'; + +export default function WebPreview() { + const { curProject } = useContext(ProjectContext); + const [url, setUrl] = useState(''); + const iframeRef = useRef(null); + + useEffect(() => { + const getWebUrl = async () => { + const projectPath = curProject.projectPath; + try { + const response = await fetch( + `/api/runProject?projectPath=${encodeURIComponent(projectPath)}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ); + const json = await response.json(); + console.log(json); + await new Promise((resolve) => setTimeout(resolve, 10000)); + setUrl(`http://${json.domain}/`); + } catch (error) { + console.error('fetching url error:', error); + } + }; + + getWebUrl(); + }, [curProject]); + + useEffect(() => { + if (iframeRef.current) { + iframeRef.current.src = url; + } + }, [url]); + + const refreshIframe = () => { + if (iframeRef.current) { + iframeRef.current.src = url; + } + }; + + const enterFullScreen = () => { + if (iframeRef.current) { + iframeRef.current.requestFullscreen(); + } + }; + + return ( +
+
+ setUrl(e.target.value)} + className="flex-1 p-2 border rounded" + /> + + +
+ +
+