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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,12 +8,15 @@ import { toast } from "sonner";
import EditorTerminal from "./terminal";
import { useTerminal } from "@/context/TerminalContext";
import { useEffect } from "react";
import { useSocket } from "@/context/SocketContext"
export default function Terminals() {
const { socket } = useSocket();
const {
terminals,
setTerminals,
socket,
createNewTerminal,
closeTerminal,
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";
import React, { createContext, useContext, useState, useEffect } from 'react';
import { io, Socket } from 'socket.io-client';
import React, { createContext, useContext, useState } from 'react';
import { Terminal } from '@xterm/xterm';
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal';
import { useSocket } from '@/context/SocketContext';
interface TerminalContextType {
socket: Socket | null;
terminals: { id: string; terminal: Terminal | null }[];
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>;
activeTerminalId: string;
@ -15,41 +14,16 @@ interface TerminalContextType {
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
createNewTerminal: (command?: string) => Promise<void>;
closeTerminal: (id: string) => void;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
deploy: (callback: () => void) => void;
}
const TerminalContext = createContext<TerminalContextType | undefined>(undefined);
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 [activeTerminalId, setActiveTerminalId] = useState<string>('');
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> => {
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) => {
if (!socket) console.error("Couldn't deploy: No socket");
console.log("Deploying...")
@ -99,7 +68,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
}
const value = {
socket,
terminals,
setTerminals,
activeTerminalId,
@ -108,7 +76,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setCreatingTerminal,
createNewTerminal,
closeTerminal,
setUserAndSandboxId,
deploy
};

View File

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