feat: add run button

This commit is contained in:
Akhilesh Rangani 2024-07-23 17:30:35 -04:00
parent 7dd67f72d8
commit deb32352fb
8 changed files with 518 additions and 298 deletions

View File

@ -6,6 +6,8 @@ import { ThemeProvider } from "@/components/layout/themeProvider"
import { ClerkProvider } from "@clerk/nextjs" 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 { PreviewProvider } from "@/context/PreviewContext"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sandbox", title: "Sandbox",
@ -27,7 +29,11 @@ export default function RootLayout({
forcedTheme="dark" forcedTheme="dark"
disableTransitionOnChange disableTransitionOnChange
> >
<PreviewProvider>
<TerminalProvider>
{children} {children}
</TerminalProvider>
</PreviewProvider>
<Analytics /> <Analytics />
<Toaster position="bottom-left" richColors /> <Toaster position="bottom-left" richColors />
</ThemeProvider> </ThemeProvider>

View File

@ -31,6 +31,8 @@ import Loading from "./loading"
import PreviewWindow from "./preview" 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 { useTerminal } from '@/context/TerminalContext';
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -50,8 +52,17 @@ export default function CodeEditor({
{ {
timeout: 2000, timeout: 2000,
} }
);} );
}
//Terminalcontext functionsand effects
const { setUserAndSandboxId } = useTerminal();
useEffect(() => {
setUserAndSandboxId(userData.id, sandboxData.id);
}, [userData.id, sandboxData.id, setUserAndSandboxId]);
//Preview Button state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
const [disableAccess, setDisableAccess] = useState({ const [disableAccess, setDisableAccess] = useState({
isDisabled: false, isDisabled: false,
@ -317,7 +328,7 @@ 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); socketRef.current?.emit("saveFile", activeFileId, value);
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY)||1000), }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
[socketRef] [socketRef]
); );
@ -425,7 +436,7 @@ export default function CodeEditor({
// Socket event listener effect // Socket event listener effect
useEffect(() => { useEffect(() => {
const onConnect = () => {} const onConnect = () => { }
const onDisconnect = () => { const onDisconnect = () => {
setTerminals([]) setTerminals([])
@ -624,7 +635,7 @@ export default function CodeEditor({
<DisableAccessModal <DisableAccessModal
message={disableAccess.message} message={disableAccess.message}
open={disableAccess.isDisabled} open={disableAccess.isDisabled}
setOpen={() => {}} setOpen={() => { }}
/> />
<Loading /> <Loading />
</> </>
@ -633,6 +644,7 @@ export default function CodeEditor({
return ( return (
<> <>
{/* Copilot DOM elements */} {/* Copilot DOM elements */}
<PreviewProvider>
<div ref={generateRef} /> <div ref={generateRef} />
<div className="z-50 p-1" ref={generateWidgetRef}> <div className="z-50 p-1" ref={generateWidgetRef}>
{generate.show && ai ? ( {generate.show && ai ? (
@ -803,7 +815,7 @@ export default function CodeEditor({
<ResizablePanel defaultSize={40}> <ResizablePanel defaultSize={40}>
<ResizablePanelGroup direction="vertical"> <ResizablePanelGroup direction="vertical">
<ResizablePanel <ResizablePanel
ref={previewPanelRef} ref={usePreview().previewPanelRef}
defaultSize={4} defaultSize={4}
collapsedSize={4} collapsedSize={4}
minSize={25} minSize={25}
@ -813,13 +825,10 @@ export default function CodeEditor({
onExpand={() => setIsPreviewCollapsed(false)} onExpand={() => setIsPreviewCollapsed(false)}
> >
<PreviewWindow <PreviewWindow
collapsed={isPreviewCollapsed}
open={() => { open={() => {
previewPanelRef.current?.expand() usePreview().previewPanelRef.current?.expand()
setIsPreviewCollapsed(false) setIsPreviewCollapsed(false)
}} } } collapsed={isPreviewCollapsed} src={previewURL}/>
src={previewURL}
/>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel <ResizablePanel
@ -828,11 +837,7 @@ export default function CodeEditor({
className="p-2 flex flex-col" className="p-2 flex flex-col"
> >
{isOwner ? ( {isOwner ? (
<Terminals <Terminals />
terminals={terminals}
setTerminals={setTerminals}
socket={socketRef.current}
/>
) : ( ) : (
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none"> <div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
<TerminalSquare className="w-4 h-4 mr-2" /> <TerminalSquare className="w-4 h-4 mr-2" />
@ -843,6 +848,7 @@ export default function CodeEditor({
</ResizablePanelGroup> </ResizablePanelGroup>
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
</PreviewProvider>
</> </>
) )
} }

View File

@ -11,6 +11,8 @@ import { useState } from "react";
import EditSandboxModal from "./edit"; import EditSandboxModal from "./edit";
import ShareSandboxModal from "./share"; import ShareSandboxModal from "./share";
import { Avatars } from "../live/avatars"; import { Avatars } from "../live/avatars";
import RunButtonModal from "./run";
import DeployButtonModal from "./deploy";
export default function Navbar({ export default function Navbar({
userData, userData,
@ -19,15 +21,13 @@ export default function Navbar({
}: { }: {
userData: User; userData: User;
sandboxData: Sandbox; sandboxData: Sandbox;
shared: { shared: { id: string; name: string }[];
id: string;
name: string;
}[];
}) { }) {
const [isEditOpen, setIsEditOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false);
const [isShareOpen, setIsShareOpen] = useState(false); const [isShareOpen, setIsShareOpen] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const isOwner = sandboxData.userId === userData.id; const isOwner = sandboxData.userId === userData.id;;
return ( return (
<> <>
@ -62,14 +62,21 @@ export default function Navbar({
) : null} ) : null}
</div> </div>
</div> </div>
<RunButtonModal
isRunning={isRunning}
setIsRunning={setIsRunning}
/>
<div className="flex items-center h-full space-x-4"> <div className="flex items-center h-full space-x-4">
<Avatars /> <Avatars />
{isOwner ? ( {isOwner ? (
<>
<DeployButtonModal />
<Button variant="outline" onClick={() => setIsShareOpen(true)}> <Button variant="outline" onClick={() => setIsShareOpen(true)}>
<Users className="w-4 h-4 mr-2" /> <Users className="w-4 h-4 mr-2" />
Share Share
</Button> </Button>
</>
) : null} ) : null}
<UserButton userData={userData} /> <UserButton userData={userData} />
</div> </div>

View File

@ -0,0 +1,59 @@
"use client";
import { Play, StopCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTerminal } from "@/context/TerminalContext";
import { usePreview } from "@/context/PreviewContext";
import { toast } from "sonner";
export default function RunButtonModal({
isRunning,
setIsRunning,
}: {
isRunning: boolean;
setIsRunning: (running: boolean) => void;
}) {
const { createNewTerminal, terminals, closeTerminal } = useTerminal();
const { setIsPreviewCollapsed, previewPanelRef} = usePreview();
const handleRun = () => {
if (isRunning) {
console.log('Stopping sandbox...');
console.log('Closing Preview Window');
terminals.forEach(term => {
if (term.terminal) {
closeTerminal(term.id);
console.log('Closing Terminal', term.id);
}
});
setIsPreviewCollapsed(true);
previewPanelRef.current?.collapse();
} else {
console.log('Running sandbox...');
console.log('Opening Terminal');
console.log('Opening Preview Window');
if (terminals.length < 4) {
createNewTerminal();
} else {
toast.error("You reached the maximum # of terminals.");
console.error('Maximum number of terminals reached.');
}
setIsPreviewCollapsed(false);
previewPanelRef.current?.expand();
}
setIsRunning(!isRunning);
};
return (
<>
<Button variant="outline" onClick={handleRun}>
{isRunning ? <StopCircle className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
{isRunning ? 'Stop' : 'Run'}
</Button>
</>
);
}

View File

@ -1,13 +1,9 @@
"use client" "use client"
import { import {
ChevronLeft,
ChevronRight,
Globe,
Link, Link,
RotateCw, RotateCw,
TerminalSquare, TerminalSquare,
UnfoldVertical,
} from "lucide-react" } from "lucide-react"
import { useRef, useState } from "react" import { useRef, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
@ -27,22 +23,22 @@ export default function PreviewWindow({
return ( return (
<> <>
<div <div
className={`${ className={`${collapsed ? "h-full" : "h-10"
collapsed ? "h-full" : "h-10"
} select-none w-full flex gap-2`} } select-none w-full flex gap-2`}
> >
<div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between"> <div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between">
<div className="text-xs">Preview</div> <div className="text-xs">Preview</div>
<div className="flex space-x-1 translate-x-1"> <div className="flex space-x-1 translate-x-1">
{collapsed ? ( {collapsed ? (
<PreviewButton onClick={open}> <PreviewButton disabled onClick={() => { }}>
<UnfoldVertical className="w-4 h-4" /> <TerminalSquare className="w-4 h-4" />
</PreviewButton> </PreviewButton>
) : ( ) : (
<> <>
{/* Todo, make this open inspector */} {/* Removed the unfoldvertical button since we have the same thing via the run button.
{/* <PreviewButton disabled onClick={() => {}}>
<TerminalSquare className="w-4 h-4" /> <PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" />
</PreviewButton> */} </PreviewButton> */}
<PreviewButton <PreviewButton
@ -94,8 +90,7 @@ function PreviewButton({
}) { }) {
return ( return (
<div <div
className={`${ className={`${disabled ? "pointer-events-none opacity-50" : ""
disabled ? "pointer-events-none opacity-50" : ""
} p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`} } p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`}
onClick={onClick} onClick={onClick}
> >

View File

@ -2,55 +2,62 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Tab from "@/components/ui/tab"; import Tab from "@/components/ui/tab";
import { closeTerminal, createTerminal } from "@/lib/terminal";
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm";
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react"; import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react";
import { Socket } from "socket.io-client";
import { toast } from "sonner"; import { toast } from "sonner";
import EditorTerminal from "./terminal"; import EditorTerminal from "./terminal";
import { useState } from "react"; import { useTerminal } from "@/context/TerminalContext";
import { useEffect } from "react";
export default function Terminals({ export default function Terminals() {
const {
terminals, terminals,
setTerminals, setTerminals,
socket, socket,
}: { createNewTerminal,
terminals: { id: string; terminal: Terminal | null }[]; closeTerminal,
setTerminals: React.Dispatch< activeTerminalId,
React.SetStateAction< setActiveTerminalId,
{ creatingTerminal,
id: string; } = useTerminal();
terminal: Terminal | null;
}[]
>
>;
socket: Socket;
}) {
const [activeTerminalId, setActiveTerminalId] = useState("");
const [creatingTerminal, setCreatingTerminal] = useState(false);
const [closingTerminal, setClosingTerminal] = useState("");
const activeTerminal = terminals.find((t) => t.id === activeTerminalId); const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
// Effect to set the active terminal when a new one is created
useEffect(() => {
if (terminals.length > 0 && !activeTerminalId) {
setActiveTerminalId(terminals[terminals.length - 1].id);
}
}, [terminals, activeTerminalId, setActiveTerminalId]);
const handleCreateTerminal = () => {
if (terminals.length >= 4) {
toast.error("You reached the maximum # of terminals.");
return;
}
createNewTerminal();
};
const handleCloseTerminal = (termId: string) => {
closeTerminal(termId);
if (activeTerminalId === termId) {
const remainingTerminals = terminals.filter(t => t.id !== termId);
if (remainingTerminals.length > 0) {
setActiveTerminalId(remainingTerminals[0].id);
} else {
setActiveTerminalId("");
}
}
};
return ( return (
<> <>
<div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll"> <div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll">
{terminals.map((term) => ( {terminals.map((term) => (
<Tab <Tab
key={term.id} key={term.id}
creating={creatingTerminal}
onClick={() => setActiveTerminalId(term.id)} onClick={() => setActiveTerminalId(term.id)}
onClose={() => onClose={() => handleCloseTerminal(term.id)}
closeTerminal({
term,
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal,
socket,
activeTerminalId,
})
}
closing={closingTerminal === term.id}
selected={activeTerminalId === term.id} selected={activeTerminalId === term.id}
> >
<SquareTerminal className="w-4 h-4 mr-2" /> <SquareTerminal className="w-4 h-4 mr-2" />
@ -59,18 +66,7 @@ export default function Terminals({
))} ))}
<Button <Button
disabled={creatingTerminal} disabled={creatingTerminal}
onClick={() => { onClick={handleCreateTerminal}
if (terminals.length >= 4) {
toast.error("You reached the maximum # of terminals.");
return;
}
createTerminal({
setTerminals,
setActiveTerminalId,
setCreatingTerminal,
socket,
});
}}
size="smIcon" size="smIcon"
variant={"secondary"} variant={"secondary"}
className={`font-normal shrink-0 select-none text-muted-foreground disabled:opacity-50`} className={`font-normal shrink-0 select-none text-muted-foreground disabled:opacity-50`}
@ -92,10 +88,10 @@ export default function Terminals({
term={term.terminal} term={term.terminal}
setTerm={(t: Terminal) => { setTerm={(t: Terminal) => {
setTerminals((prev) => setTerminals((prev) =>
prev.map((term) => prev.map((prevTerm) =>
term.id === activeTerminalId prevTerm.id === term.id
? { ...term, terminal: t } ? { ...prevTerm, terminal: t }
: term : prevTerm
) )
); );
}} }}

View File

@ -0,0 +1,34 @@
"use client"
import React, { createContext, useContext, useState, useRef } from 'react';
import { ImperativePanelHandle } from "react-resizable-panels";
interface PreviewContextType {
isPreviewCollapsed: boolean;
setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
previewURL: string;
setPreviewURL: React.Dispatch<React.SetStateAction<string>>;
previewPanelRef: React.RefObject<ImperativePanelHandle>;
}
const PreviewContext = createContext<PreviewContextType | undefined>(undefined);
export const PreviewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true);
const [previewURL, setPreviewURL] = useState<string>("");
const previewPanelRef = useRef<ImperativePanelHandle>(null);
return (
<PreviewContext.Provider value={{ isPreviewCollapsed, setIsPreviewCollapsed, previewURL, setPreviewURL, previewPanelRef }}>
{children}
</PreviewContext.Provider>
);
};
export const usePreview = () => {
const context = useContext(PreviewContext);
if (context === undefined) {
throw new Error('usePreview must be used within a PreviewProvider');
}
return context;
};

View File

@ -0,0 +1,117 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from 'react';
import { io, Socket } from 'socket.io-client';
import { Terminal } from '@xterm/xterm';
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal'; // Adjust the import path as necessary
interface TerminalContextType {
socket: Socket | null;
terminals: { id: string; terminal: Terminal | null }[];
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>;
activeTerminalId: string;
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
creatingTerminal: boolean;
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
createNewTerminal: () => void;
closeTerminal: (id: string) => void;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
}
const TerminalContext = createContext<TerminalContextType | undefined>(undefined);
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null);
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(`${window.location.protocol}//${window.location.hostname}:${process.env.NEXT_PUBLIC_SERVER_PORT}?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 () => {
if (!socket) return;
setCreatingTerminal(true);
try {
createTerminalHelper({
setTerminals,
setActiveTerminalId,
setCreatingTerminal,
socket,
});
} catch (error) {
console.error("Error creating terminal:", error);
} finally {
setCreatingTerminal(false);
}
};
const closeTerminal = (id: string) => {
if (!socket) return;
const terminalToClose = terminals.find(term => term.id === id);
if (terminalToClose) {
closeTerminalHelper({
term: terminalToClose,
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal: () => {},
socket,
activeTerminalId,
});
}
};
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
const value = {
socket,
terminals,
setTerminals,
activeTerminalId,
setActiveTerminalId,
creatingTerminal,
setCreatingTerminal,
createNewTerminal,
closeTerminal,
setUserAndSandboxId,
};
return (
<TerminalContext.Provider value={value}>
{children}
</TerminalContext.Provider>
);
};
export const useTerminal = (): TerminalContextType => {
const context = useContext(TerminalContext);
if (!context) {
throw new Error('useTerminal must be used within a TerminalProvider');
}
return context;
};