Compare commits

...

7 Commits

Author SHA1 Message Date
6be2c02506 fix: socket connection 2024-09-01 22:04:56 -04:00
1d92b6ea6f chore: removed logs, added comments 2024-09-01 20:06:11 -04:00
f683ff6480 fix: files not loading when creating a new project
This push contains console logs at various places where the server is emitting the event and the client is receiving the event. Please remove those before merging with production.
2024-08-31 20:31:20 -04:00
2da60ff4e4 fix: only one socket connection via socketcontext 2024-08-23 20:09:54 -04:00
ae7ff3f46b fix: types mismatch 2024-08-19 21:17:30 -04:00
7559e9804f feat: different run commands based on file types 2024-08-19 20:39:04 -04:00
5132850cb0 fix: remove undefined type 2024-08-18 12:37:17 -07:00
11 changed files with 134 additions and 98 deletions

View File

@ -7,7 +7,8 @@ import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import { Analytics } from "@vercel/analytics/react" import { Analytics } from "@vercel/analytics/react"
import { TerminalProvider } from '@/context/TerminalContext'; import { TerminalProvider } from '@/context/TerminalContext';
import { PreviewProvider } from "@/context/PreviewContext" import { PreviewProvider } from "@/context/PreviewContext";
import { SocketProvider } from '@/context/SocketContext'
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sandbox", title: "Sandbox",
@ -15,7 +16,7 @@ export const metadata: Metadata = {
} }
export default function RootLayout({ export default function RootLayout({
children, children
}: Readonly<{ }: Readonly<{
children: React.ReactNode children: React.ReactNode
}>) { }>) {
@ -29,11 +30,13 @@ export default function RootLayout({
forcedTheme="dark" forcedTheme="dark"
disableTransitionOnChange disableTransitionOnChange
> >
<SocketProvider>
<PreviewProvider> <PreviewProvider>
<TerminalProvider> <TerminalProvider>
{children} {children}
</TerminalProvider> </TerminalProvider>
</PreviewProvider> </PreviewProvider>
</SocketProvider>
<Analytics /> <Analytics />
<Toaster position="bottom-left" richColors /> <Toaster position="bottom-left" richColors />
</ThemeProvider> </ThemeProvider>

View File

@ -49,10 +49,8 @@ export default function Dashboard({
const q = searchParams.get("q") const q = searchParams.get("q")
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => { // update the dashboard to show a new project
if (!sandboxes) {
router.refresh() router.refresh()
}
}, [sandboxes]) }, [sandboxes])
return ( return (

View File

@ -93,7 +93,7 @@ export default function NewProjectModal({
open: boolean open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
}) { }) {
const [selected, setSelected] = useState<TOptions>("react") const [selected, setSelected] = useState("react")
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const router = useRouter() const router = useRouter()

View File

@ -12,7 +12,7 @@ import { toast } from "sonner";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { CanvasRevealEffect } from "./projectCard/revealEffect"; import { CanvasRevealEffect } from "./projectCard/revealEffect";
const colors = { const colors: { [key: string]: number[][] } = {
react: [ react: [
[71, 207, 237], [71, 207, 237],
[30, 126, 148], [30, 126, 148],

View File

@ -3,7 +3,6 @@
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react" import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
import monaco from "monaco-editor" import monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react" import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { Socket, io } from "socket.io-client"
import { toast } from "sonner" import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs"
@ -32,7 +31,7 @@ import PreviewWindow from "./preview"
import Terminals from "./terminals" import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels" import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from '@/context/PreviewContext'; import { PreviewProvider, usePreview } from '@/context/PreviewContext';
import { useTerminal } from '@/context/TerminalContext'; import { useSocket } from "@/context/SocketContext"
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -41,24 +40,20 @@ export default function CodeEditor({
userData: User userData: User
sandboxData: Sandbox sandboxData: Sandbox
}) { }) {
const socketRef = useRef<Socket | null>(null);
// Initialize socket connection if it doesn't exist //SocketContext functions and effects
if (!socketRef.current) { const { socket, setUserAndSandboxId } = useSocket();
socketRef.current = io(
`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userData.id}&sandboxId=${sandboxData.id}`,
{
timeout: 2000,
}
);
}
//Terminalcontext functionsand effects
const { setUserAndSandboxId } = useTerminal();
useEffect(() => { useEffect(() => {
// Ensure userData.id and sandboxData.id are available before attempting to connect
if (userData.id && sandboxData.id) {
// Check if the socket is not initialized or not connected
if (!socket || (socket && !socket.connected)) {
// Initialize socket connection
setUserAndSandboxId(userData.id, sandboxData.id); setUserAndSandboxId(userData.id, sandboxData.id);
}, [userData.id, sandboxData.id, setUserAndSandboxId]); }
}
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId]);
//Preview Button state //Preview Button state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
@ -333,9 +328,9 @@ export default function CodeEditor({
); );
console.log(`Saving file...${activeFileId}`); console.log(`Saving file...${activeFileId}`);
console.log(`Saving file...${value}`); console.log(`Saving file...${value}`);
socketRef.current?.emit("saveFile", activeFileId, value); socket?.emit("saveFile", activeFileId, value);
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
[socketRef] [socket]
); );
useEffect(() => { useEffect(() => {
@ -432,11 +427,11 @@ export default function CodeEditor({
// Connection/disconnection effect // Connection/disconnection effect
useEffect(() => { useEffect(() => {
socketRef.current?.connect() socket?.connect()
return () => { return () => {
socketRef.current?.disconnect() socket?.disconnect()
} }
}, []) }, [socket])
// Socket event listener effect // Socket event listener effect
useEffect(() => { useEffect(() => {
@ -469,25 +464,24 @@ export default function CodeEditor({
}) })
} }
socketRef.current?.on("connect", onConnect) socket?.on("connect", onConnect)
socketRef.current?.on("disconnect", onDisconnect) socket?.on("disconnect", onDisconnect)
socketRef.current?.on("loaded", onLoadedEvent) socket?.on("loaded", onLoadedEvent)
socketRef.current?.on("error", onError) socket?.on("error", onError)
socketRef.current?.on("terminalResponse", onTerminalResponse) socket?.on("terminalResponse", onTerminalResponse)
socketRef.current?.on("disableAccess", onDisableAccess) socket?.on("disableAccess", onDisableAccess)
socketRef.current?.on("previewURL", loadPreviewURL) socket?.on("previewURL", loadPreviewURL)
return () => { return () => {
socketRef.current?.off("connect", onConnect) socket?.off("connect", onConnect)
socketRef.current?.off("disconnect", onDisconnect) socket?.off("disconnect", onDisconnect)
socketRef.current?.off("loaded", onLoadedEvent) socket?.off("loaded", onLoadedEvent)
socketRef.current?.off("error", onError) socket?.off("error", onError)
socketRef.current?.off("terminalResponse", onTerminalResponse) socket?.off("terminalResponse", onTerminalResponse)
socketRef.current?.off("disableAccess", onDisableAccess) socket?.off("disableAccess", onDisableAccess)
socketRef.current?.off("previewURL", loadPreviewURL) socket?.off("previewURL", loadPreviewURL)
} }
// }, []); }, [socket, terminals, setTerminals, setFiles, toast, setDisableAccess, isOwner, loadPreviewURL]);
}, [terminals])
// Helper functions for tabs: // Helper functions for tabs:
@ -499,7 +493,7 @@ export default function CodeEditor({
// Debounced function to get file content // Debounced function to get file content
const debouncedGetFile = useCallback( const debouncedGetFile = useCallback(
debounce((tabId, callback) => { debounce((tabId, callback) => {
socketRef.current?.emit('getFile', tabId, callback); socket?.emit('getFile', tabId, callback);
}, 300), // 300ms debounce delay, adjust as needed }, 300), // 300ms debounce delay, adjust as needed
[] []
); );
@ -603,7 +597,7 @@ export default function CodeEditor({
return false return false
} }
socketRef.current?.emit("renameFile", id, newName) socket?.emit("renameFile", id, newName)
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab)) prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
) )
@ -612,7 +606,7 @@ export default function CodeEditor({
} }
const handleDeleteFile = (file: TFile) => { const handleDeleteFile = (file: TFile) => {
socketRef.current?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => { socket?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
setFiles(response) setFiles(response)
}) })
closeTab(file.id) closeTab(file.id)
@ -622,11 +616,11 @@ export default function CodeEditor({
setDeletingFolderId(folder.id) setDeletingFolderId(folder.id)
console.log("deleting folder", folder.id) console.log("deleting folder", folder.id)
socketRef.current?.emit("getFolder", folder.id, (response: string[]) => socket?.emit("getFolder", folder.id, (response: string[]) =>
closeTabs(response) closeTabs(response)
) )
socketRef.current?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => { socket?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
setFiles(response) setFiles(response)
setDeletingFolderId("") setDeletingFolderId("")
}) })
@ -654,7 +648,7 @@ export default function CodeEditor({
{generate.show && ai ? ( {generate.show && ai ? (
<GenerateInput <GenerateInput
user={userData} user={userData}
socket={socketRef.current} socket={socket!}
width={generate.width - 90} width={generate.width - 90}
data={{ data={{
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "", fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
@ -714,7 +708,7 @@ export default function CodeEditor({
handleRename={handleRename} handleRename={handleRename}
handleDeleteFile={handleDeleteFile} handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder} handleDeleteFolder={handleDeleteFolder}
socket={socketRef.current} socket={socket!}
setFiles={setFiles} setFiles={setFiles}
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)} addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
deletingFolderId={deletingFolderId} deletingFolderId={deletingFolderId}

View File

@ -65,6 +65,7 @@ export default function Navbar({
<RunButtonModal <RunButtonModal
isRunning={isRunning} isRunning={isRunning}
setIsRunning={setIsRunning} setIsRunning={setIsRunning}
sandboxData={sandboxData}
/> />
<div className="flex items-center h-full space-x-4"> <div className="flex items-center h-full space-x-4">
<Avatars /> <Avatars />

View File

@ -5,13 +5,16 @@ import { Button } from "@/components/ui/button";
import { useTerminal } from "@/context/TerminalContext"; import { useTerminal } from "@/context/TerminalContext";
import { usePreview } from "@/context/PreviewContext"; import { usePreview } from "@/context/PreviewContext";
import { toast } from "sonner"; import { toast } from "sonner";
import { Sandbox } from "@/lib/types";
export default function RunButtonModal({ export default function RunButtonModal({
isRunning, isRunning,
setIsRunning, setIsRunning,
sandboxData,
}: { }: {
isRunning: boolean; isRunning: boolean;
setIsRunning: (running: boolean) => void; setIsRunning: (running: boolean) => void;
sandboxData: Sandbox;
}) { }) {
const { createNewTerminal, terminals, closeTerminal } = useTerminal(); const { createNewTerminal, terminals, closeTerminal } = useTerminal();
const { setIsPreviewCollapsed, previewPanelRef } = usePreview(); const { setIsPreviewCollapsed, previewPanelRef } = usePreview();
@ -36,12 +39,16 @@ export default function RunButtonModal({
console.log('Opening Preview Window'); console.log('Opening Preview Window');
if (terminals.length < 4) { if (terminals.length < 4) {
createNewTerminal("yarn install && yarn start"); if (sandboxData.type === "streamlit") {
// For testing: createNewTerminal(
//createNewTerminal("echo http://localhost:3000"); "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
);
} else {
createNewTerminal("yarn install && yarn dev");
}
} else { } else {
toast.error("You reached the maximum # of terminals."); toast.error("You reached the maximum # of terminals.");
console.error('Maximum number of terminals reached.'); console.error("Maximum number of terminals reached.");
} }
setIsPreviewCollapsed(false); setIsPreviewCollapsed(false);

View File

@ -8,12 +8,15 @@ import { toast } from "sonner";
import EditorTerminal from "./terminal"; import EditorTerminal from "./terminal";
import { useTerminal } from "@/context/TerminalContext"; import { useTerminal } from "@/context/TerminalContext";
import { useEffect } from "react"; import { useEffect } from "react";
import { useSocket } from "@/context/SocketContext"
export default function Terminals() { export default function Terminals() {
const { socket } = useSocket();
const { const {
terminals, terminals,
setTerminals, setTerminals,
socket,
createNewTerminal, createNewTerminal,
closeTerminal, closeTerminal,
activeTerminalId, activeTerminalId,

View File

@ -0,0 +1,63 @@
"use client";
import React, { createContext, useContext, useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
interface SocketContextType {
socket: Socket | null;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
}
const SocketContext = createContext<SocketContextType | undefined>(undefined);
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [sandboxId, setSandboxId] = useState<string | null>(null);
useEffect(() => {
if (userId && sandboxId) {
console.log("Initializing socket connection...");
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
console.log("Socket instance:", newSocket);
setSocket(newSocket);
newSocket.on('connect', () => {
console.log("Socket connected:", newSocket.id);
});
newSocket.on('disconnect', () => {
console.log("Socket disconnected");
});
return () => {
console.log("Disconnecting socket...");
newSocket.disconnect();
};
}
}, [userId, sandboxId]);
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
const value = {
socket,
setUserAndSandboxId,
};
return (
<SocketContext.Provider value={ value }>
{children}
</SocketContext.Provider>
);
};
export const useSocket = (): SocketContextType => {
const context = useContext(SocketContext);
if (!context) {
throw new Error('useSocket must be used within a SocketProvider');
}
return context;
};

View File

@ -1,12 +1,11 @@
"use client"; "use client";
import React, { createContext, useContext, useState, useEffect } from 'react'; import React, { createContext, useContext, useState } from 'react';
import { io, Socket } from 'socket.io-client';
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal'; import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal';
import { useSocket } from '@/context/SocketContext';
interface TerminalContextType { interface TerminalContextType {
socket: Socket | null;
terminals: { id: string; terminal: Terminal | null }[]; terminals: { id: string; terminal: Terminal | null }[];
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>; setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>;
activeTerminalId: string; activeTerminalId: string;
@ -15,41 +14,16 @@ interface TerminalContextType {
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>; setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
createNewTerminal: (command?: string) => Promise<void>; createNewTerminal: (command?: string) => Promise<void>;
closeTerminal: (id: string) => void; closeTerminal: (id: string) => void;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
deploy: (callback: () => void) => void; deploy: (callback: () => void) => void;
} }
const TerminalContext = createContext<TerminalContextType | undefined>(undefined); const TerminalContext = createContext<TerminalContextType | undefined>(undefined);
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null); const { socket } = useSocket();
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]); const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]);
const [activeTerminalId, setActiveTerminalId] = useState<string>(''); const [activeTerminalId, setActiveTerminalId] = useState<string>('');
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false); const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false);
const [userId, setUserId] = useState<string | null>(null);
const [sandboxId, setSandboxId] = useState<string | null>(null);
useEffect(() => {
if (userId && sandboxId) {
console.log("Initializing socket connection...");
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
console.log("Socket instance:", newSocket);
setSocket(newSocket);
newSocket.on('connect', () => {
console.log("Socket connected:", newSocket.id);
});
newSocket.on('disconnect', () => {
console.log("Socket disconnected");
});
return () => {
console.log("Disconnecting socket...");
newSocket.disconnect();
};
}
}, [userId, sandboxId]);
const createNewTerminal = async (command?: string): Promise<void> => { const createNewTerminal = async (command?: string): Promise<void> => {
if (!socket) return; if (!socket) return;
@ -85,11 +59,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
} }
}; };
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
const deploy = (callback: () => void) => { const deploy = (callback: () => void) => {
if (!socket) console.error("Couldn't deploy: No socket"); if (!socket) console.error("Couldn't deploy: No socket");
console.log("Deploying...") console.log("Deploying...")
@ -99,7 +68,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
} }
const value = { const value = {
socket,
terminals, terminals,
setTerminals, setTerminals,
activeTerminalId, activeTerminalId,
@ -108,7 +76,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setCreatingTerminal, setCreatingTerminal,
createNewTerminal, createNewTerminal,
closeTerminal, closeTerminal,
setUserAndSandboxId,
deploy deploy
}; };

View File

@ -12,7 +12,7 @@ export type User = {
export type Sandbox = { export type Sandbox = {
id: string; id: string;
name: string; name: string;
type: "react" | "node"; type: string;
visibility: "public" | "private"; visibility: "public" | "private";
createdAt: Date; createdAt: Date;
userId: string; userId: string;