Merge branch 'refs/heads/feat/dokku' into production

# Conflicts:
#	frontend/app/layout.tsx
This commit is contained in:
James Murdza
2024-07-31 18:18:38 -07:00
23 changed files with 1443 additions and 347 deletions

View File

@ -31,6 +31,8 @@ import Loading from "./loading"
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';
export default function CodeEditor({
userData,
@ -48,8 +50,17 @@ export default function CodeEditor({
{
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 [disableAccess, setDisableAccess] = useState({
isDisabled: false,
@ -315,7 +326,7 @@ export default function CodeEditor({
console.log(`Saving file...${activeFileId}`);
console.log(`Saving file...${value}`);
socketRef.current?.emit("saveFile", activeFileId, value);
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY)||1000),
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
[socketRef]
);
@ -341,7 +352,7 @@ export default function CodeEditor({
if (!editorRef || !tab || !model) return
let providerData: ProviderData;
// When a file is opened for the first time, create a new provider and store in providersMap.
if (!providersMap.current.has(tab.id)) {
const yDoc = new Y.Doc();
@ -383,7 +394,6 @@ export default function CodeEditor({
);
providerData.binding = binding;
setProvider(providerData.provider);
return () => {
@ -397,25 +407,24 @@ export default function CodeEditor({
};
}, [room, activeFileContent]);
// Added this effect to clean up when the component unmounts
useEffect(() => {
return () => {
// Clean up all providers when the component unmounts
providersMap.current.forEach((data) => {
if (data.binding) {
data.binding.destroy();
}
data.provider.disconnect();
data.yDoc.destroy();
});
providersMap.current.clear();
};
}, []);
// Added this effect to clean up when the component unmounts
useEffect(() => {
return () => {
// Clean up all providers when the component unmounts
providersMap.current.forEach((data) => {
if (data.binding) {
data.binding.destroy();
}
data.provider.disconnect();
data.yDoc.destroy();
});
providersMap.current.clear();
};
}, []);
// Connection/disconnection effect
useEffect(() => {
socketRef.current?.connect()
return () => {
socketRef.current?.disconnect()
}
@ -423,7 +432,7 @@ export default function CodeEditor({
// Socket event listener effect
useEffect(() => {
const onConnect = () => {}
const onConnect = () => { }
const onDisconnect = () => {
setTerminals([])
@ -528,8 +537,8 @@ export default function CodeEditor({
? numTabs === 1
? null
: index < numTabs - 1
? tabs[index + 1].id
: tabs[index - 1].id
? tabs[index + 1].id
: tabs[index - 1].id
: activeFileId
setTabs((prev) => prev.filter((t) => t.id !== id))
@ -622,7 +631,7 @@ export default function CodeEditor({
<DisableAccessModal
message={disableAccess.message}
open={disableAccess.isDisabled}
setOpen={() => {}}
setOpen={() => { }}
/>
<Loading />
</>
@ -631,216 +640,211 @@ export default function CodeEditor({
return (
<>
{/* Copilot DOM elements */}
<div ref={generateRef} />
<div className="z-50 p-1" ref={generateWidgetRef}>
{generate.show && ai ? (
<GenerateInput
user={userData}
socket={socketRef.current}
width={generate.width - 90}
data={{
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
code: editorRef?.getValue() ?? "",
line: generate.line,
}}
editor={{
language: editorLanguage,
}}
onExpand={() => {
editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id)
<PreviewProvider>
<div ref={generateRef} />
<div className="z-50 p-1" ref={generateWidgetRef}>
{generate.show && ai ? (
<GenerateInput
user={userData}
socket={socketRef.current}
width={generate.width - 90}
data={{
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
code: editorRef?.getValue() ?? "",
line: generate.line,
}}
editor={{
language: editorLanguage,
}}
onExpand={() => {
editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id)
if (!generateRef.current) return
const id = changeAccessor.addZone({
afterLineNumber: cursorLine,
heightInLines: 12,
domNode: generateRef.current,
if (!generateRef.current) return
const id = changeAccessor.addZone({
afterLineNumber: cursorLine,
heightInLines: 12,
domNode: generateRef.current,
})
setGenerate((prev) => {
return { ...prev, id }
})
})
}}
onAccept={(code: string) => {
const line = generate.line
setGenerate((prev) => {
return { ...prev, id }
return {
...prev,
show: !prev.show,
}
})
})
}}
onAccept={(code: string) => {
const line = generate.line
setGenerate((prev) => {
return {
...prev,
show: !prev.show,
}
})
const file = editorRef?.getValue()
const file = editorRef?.getValue()
const lines = file?.split("\n") || []
lines.splice(line - 1, 0, code)
const updatedFile = lines.join("\n")
editorRef?.setValue(updatedFile)
}}
onClose={() => {
setGenerate((prev) => {
return {
...prev,
show: !prev.show,
}
})
}}
/>
) : null}
</div>
const lines = file?.split("\n") || []
lines.splice(line - 1, 0, code)
const updatedFile = lines.join("\n")
editorRef?.setValue(updatedFile)
}}
onClose={() => {
setGenerate((prev) => {
return {
...prev,
show: !prev.show,
}
})
}}
/>
) : null}
</div>
{/* Main editor components */}
<Sidebar
sandboxData={sandboxData}
files={files}
selectFile={selectFile}
handleRename={handleRename}
handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder}
socket={socketRef.current}
setFiles={setFiles}
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
deletingFolderId={deletingFolderId}
// AI Copilot Toggle
ai={ai}
setAi={setAi}
/>
{/* Main editor components */}
<Sidebar
sandboxData={sandboxData}
files={files}
selectFile={selectFile}
handleRename={handleRename}
handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder}
socket={socketRef.current}
setFiles={setFiles}
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
deletingFolderId={deletingFolderId}
// AI Copilot Toggle
ai={ai}
setAi={setAi}
/>
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
className="p-2 flex flex-col"
maxSize={80}
minSize={30}
defaultSize={60}
ref={editorPanelRef}
>
<div className="h-10 w-full flex gap-2 overflow-auto tab-scroll">
{/* File tabs */}
{tabs.map((tab) => (
<Tab
key={tab.id}
saved={tab.saved}
selected={activeFileId === tab.id}
onClick={(e) => {
selectFile(tab)
}}
onClose={() => closeTab(tab.id)}
>
{tab.name}
</Tab>
))}
</div>
{/* Monaco editor */}
<div
ref={editorContainerRef}
className="grow w-full overflow-hidden rounded-md relative"
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
className="p-2 flex flex-col"
maxSize={80}
minSize={30}
defaultSize={60}
ref={editorPanelRef}
>
{!activeFileId ? (
<>
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
<FileJson className="w-6 h-6 mr-3" />
No file selected.
</div>
</>
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
clerk.loaded ? (
<>
{provider && userInfo ? (
<Cursors yProvider={provider} userInfo={userInfo} />
) : null}
<Editor
height="100%"
language={editorLanguage}
beforeMount={handleEditorWillMount}
onMount={handleEditorMount}
onChange={(value) => {
if (value === activeFileContent) {
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: true }
: tab
)
)
} else {
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: false }
: tab
)
)
}
<div className="h-10 w-full flex gap-2 overflow-auto tab-scroll">
{/* File tabs */}
{tabs.map((tab) => (
<Tab
key={tab.id}
saved={tab.saved}
selected={activeFileId === tab.id}
onClick={(e) => {
selectFile(tab)
}}
options={{
tabSize: 2,
minimap: {
enabled: false,
},
padding: {
bottom: 4,
top: 4,
},
scrollBeyondLastLine: false,
fixedOverflowWidgets: true,
fontFamily: "var(--font-geist-mono)",
}}
theme="vs-dark"
value={activeFileContent}
/>
</>
) : (
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
<Loader2 className="animate-spin w-6 h-6 mr-3" />
Waiting for Clerk to load...
</div>
)}
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={40}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel
ref={previewPanelRef}
defaultSize={4}
collapsedSize={4}
minSize={25}
collapsible
className="p-2 flex flex-col"
onCollapse={() => setIsPreviewCollapsed(true)}
onExpand={() => setIsPreviewCollapsed(false)}
onClose={() => closeTab(tab.id)}
>
{tab.name}
</Tab>
))}
</div>
{/* Monaco editor */}
<div
ref={editorContainerRef}
className="grow w-full overflow-hidden rounded-md relative"
>
<PreviewWindow
collapsed={isPreviewCollapsed}
open={() => {
previewPanelRef.current?.expand()
setIsPreviewCollapsed(false)
}}
src={previewURL}
/>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel
defaultSize={50}
minSize={20}
className="p-2 flex flex-col"
>
{isOwner ? (
<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">
<TerminalSquare className="w-4 h-4 mr-2" />
No terminal access.
</div>
)}
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
{!activeFileId ? (
<>
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
<FileJson className="w-6 h-6 mr-3" />
No file selected.
</div>
</>
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
clerk.loaded ? (
<>
{provider && userInfo ? (
<Cursors yProvider={provider} userInfo={userInfo} />
) : null}
<Editor
height="100%"
language={editorLanguage}
beforeMount={handleEditorWillMount}
onMount={handleEditorMount}
onChange={(value) => {
if (value === activeFileContent) {
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: true }
: tab
)
)
} else {
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: false }
: tab
)
)
}
}}
options={{
tabSize: 2,
minimap: {
enabled: false,
},
padding: {
bottom: 4,
top: 4,
},
scrollBeyondLastLine: false,
fixedOverflowWidgets: true,
fontFamily: "var(--font-geist-mono)",
}}
theme="vs-dark"
value={activeFileContent}
/>
</>
) : (
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
<Loader2 className="animate-spin w-6 h-6 mr-3" />
Waiting for Clerk to load...
</div>
)}
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={40}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel
ref={usePreview().previewPanelRef}
defaultSize={4}
collapsedSize={4}
minSize={25}
collapsible
className="p-2 flex flex-col"
onCollapse={() => setIsPreviewCollapsed(true)}
onExpand={() => setIsPreviewCollapsed(false)}
>
<PreviewWindow
open={() => {
usePreview().previewPanelRef.current?.expand()
setIsPreviewCollapsed(false)
} } collapsed={isPreviewCollapsed} src={previewURL}/>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel
defaultSize={50}
minSize={20}
className="p-2 flex flex-col"
>
{isOwner ? (
<Terminals />
) : (
<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" />
No terminal access.
</div>
)}
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</PreviewProvider>
</>
)
}

View File

@ -0,0 +1,28 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Play, Pause } from "lucide-react";
export default function DeployButtonModal() {
const [isDeploying, setIsDeploying] = useState(false);
const handleDeploy = () => {
if (isDeploying) {
console.log("Stopping deployment...");
} else {
console.log("Starting deployment...");
}
setIsDeploying(!isDeploying);
};
return (
<>
<Button variant="outline" onClick={handleDeploy}>
{isDeploying ? <Pause className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
{isDeploying ? "Deployed" : "Deploy"}
</Button>
</>
);
}

View File

@ -11,6 +11,8 @@ import { useState } from "react";
import EditSandboxModal from "./edit";
import ShareSandboxModal from "./share";
import { Avatars } from "../live/avatars";
import RunButtonModal from "./run";
import DeployButtonModal from "./deploy";
export default function Navbar({
userData,
@ -19,15 +21,13 @@ export default function Navbar({
}: {
userData: User;
sandboxData: Sandbox;
shared: {
id: string;
name: string;
}[];
shared: { id: string; name: string }[];
}) {
const [isEditOpen, setIsEditOpen] = 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 (
<>
@ -62,18 +62,25 @@ export default function Navbar({
) : null}
</div>
</div>
<RunButtonModal
isRunning={isRunning}
setIsRunning={setIsRunning}
/>
<div className="flex items-center h-full space-x-4">
<Avatars />
{isOwner ? (
<>
<DeployButtonModal />
<Button variant="outline" onClick={() => setIsShareOpen(true)}>
<Users className="w-4 h-4 mr-2" />
Share
</Button>
</>
) : null}
<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("yarn install && yarn start");
} 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"
import {
ChevronLeft,
ChevronRight,
Globe,
Link,
RotateCw,
TerminalSquare,
UnfoldVertical,
} from "lucide-react"
import { useRef, useState } from "react"
import { toast } from "sonner"
@ -27,22 +23,22 @@ export default function PreviewWindow({
return (
<>
<div
className={`${
collapsed ? "h-full" : "h-10"
} select-none w-full flex gap-2`}
className={`${collapsed ? "h-full" : "h-10"
} 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="text-xs">Preview</div>
<div className="flex space-x-1 translate-x-1">
{collapsed ? (
<PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" />
<PreviewButton disabled onClick={() => { }}>
<TerminalSquare className="w-4 h-4" />
</PreviewButton>
) : (
<>
{/* Todo, make this open inspector */}
{/* <PreviewButton disabled onClick={() => {}}>
<TerminalSquare className="w-4 h-4" />
{/* Removed the unfoldvertical button since we have the same thing via the run button.
<PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" />
</PreviewButton> */}
<PreviewButton
@ -94,9 +90,8 @@ function PreviewButton({
}) {
return (
<div
className={`${
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`}
className={`${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`}
onClick={onClick}
>
{children}

View File

@ -2,35 +2,42 @@
import { Button } from "@/components/ui/button";
import Tab from "@/components/ui/tab";
import { closeTerminal, createTerminal } from "@/lib/terminal";
import { Terminal } from "@xterm/xterm";
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react";
import { Socket } from "socket.io-client";
import { toast } from "sonner";
import EditorTerminal from "./terminal";
import { useState } from "react";
import { useTerminal } from "@/context/TerminalContext";
import { useEffect } from "react";
export default function Terminals() {
const {
terminals,
setTerminals,
socket,
createNewTerminal,
closeTerminal,
activeTerminalId,
setActiveTerminalId,
creatingTerminal,
} = useTerminal();
export default function Terminals({
terminals,
setTerminals,
socket,
}: {
terminals: { id: string; terminal: Terminal | null }[];
setTerminals: React.Dispatch<
React.SetStateAction<
{
id: string;
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);
// 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();
};
return (
<>
<div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll">
@ -39,18 +46,7 @@ export default function Terminals({
key={term.id}
creating={creatingTerminal}
onClick={() => setActiveTerminalId(term.id)}
onClose={() =>
closeTerminal({
term,
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal,
socket,
activeTerminalId,
})
}
closing={closingTerminal === term.id}
onClose={() => closeTerminal(term.id)}
selected={activeTerminalId === term.id}
>
<SquareTerminal className="w-4 h-4 mr-2" />
@ -59,18 +55,7 @@ export default function Terminals({
))}
<Button
disabled={creatingTerminal}
onClick={() => {
if (terminals.length >= 4) {
toast.error("You reached the maximum # of terminals.");
return;
}
createTerminal({
setTerminals,
setActiveTerminalId,
setCreatingTerminal,
socket,
});
}}
onClick={handleCreateTerminal}
size="smIcon"
variant={"secondary"}
className={`font-normal shrink-0 select-none text-muted-foreground disabled:opacity-50`}
@ -111,4 +96,4 @@ export default function Terminals({
)}
</>
);
}
}

View File

@ -55,7 +55,6 @@ export default function EditorTerminal({
fitAddon.fit();
const disposableOnData = term.onData((data) => {
console.log("terminalData", id, data);
socket.emit("terminalData", id, data);
});
@ -74,6 +73,20 @@ export default function EditorTerminal({
};
}, [term, terminalRef.current]);
useEffect(() => {
if (!term) return;
const handleTerminalResponse = (response: { id: string; data: string }) => {
if (response.id === id) {
term.write(response.data);
}
};
socket.on("terminalResponse", handleTerminalResponse);
return () => {
socket.off("terminalResponse", handleTerminalResponse);
};
}, [term, id, socket]);
return (
<>
<div