improve starting server logic
This commit is contained in:
parent
07d59cf01b
commit
010a4fec59
@ -3,6 +3,8 @@ CLERK_SECRET_KEY=
|
|||||||
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
|
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
|
||||||
LIVEBLOCKS_SECRET_KEY=
|
LIVEBLOCKS_SECRET_KEY=
|
||||||
|
|
||||||
|
VERCEL_ENV=development
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
AWS_ACCESS_KEY_ID=
|
||||||
AWS_SECRET_ACCESS_KEY=
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
|
||||||
|
@ -1,79 +1,75 @@
|
|||||||
import Navbar from "@/components/editor/navbar";
|
import Navbar from "@/components/editor/navbar"
|
||||||
import { Room } from "@/components/editor/live/room";
|
import { Room } from "@/components/editor/live/room"
|
||||||
import { Sandbox, User, UsersToSandboxes } from "@/lib/types";
|
import { Sandbox, User, UsersToSandboxes } from "@/lib/types"
|
||||||
import { currentUser } from "@clerk/nextjs";
|
import { currentUser } from "@clerk/nextjs"
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation"
|
||||||
import Editor from "@/components/editor";
|
import Editor from "@/components/editor"
|
||||||
|
|
||||||
export const revalidate = 0;
|
export const revalidate = 0
|
||||||
|
|
||||||
const getUserData = async (id: string) => {
|
const getUserData = async (id: string) => {
|
||||||
const userRes = await fetch(
|
const userRes = await fetch(
|
||||||
`https://database.ishaan1013.workers.dev/api/user?id=${id}`
|
`https://database.ishaan1013.workers.dev/api/user?id=${id}`
|
||||||
);
|
)
|
||||||
const userData: User = await userRes.json();
|
const userData: User = await userRes.json()
|
||||||
return userData;
|
return userData
|
||||||
};
|
}
|
||||||
|
|
||||||
const getSandboxData = async (id: string) => {
|
const getSandboxData = async (id: string) => {
|
||||||
const sandboxRes = await fetch(
|
const sandboxRes = await fetch(
|
||||||
`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`
|
`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`
|
||||||
);
|
)
|
||||||
const sandboxData: Sandbox = await sandboxRes.json();
|
const sandboxData: Sandbox = await sandboxRes.json()
|
||||||
return sandboxData;
|
return sandboxData
|
||||||
};
|
}
|
||||||
|
|
||||||
const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => {
|
const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => {
|
||||||
if (!usersToSandboxes) {
|
if (!usersToSandboxes) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const shared = await Promise.all(
|
const shared = await Promise.all(
|
||||||
usersToSandboxes.map(async (user) => {
|
usersToSandboxes.map(async (user) => {
|
||||||
const userRes = await fetch(
|
const userRes = await fetch(
|
||||||
`https://database.ishaan1013.workers.dev/api/user?id=${user.userId}`
|
`https://database.ishaan1013.workers.dev/api/user?id=${user.userId}`
|
||||||
);
|
)
|
||||||
const userData: User = await userRes.json();
|
const userData: User = await userRes.json()
|
||||||
return { id: userData.id, name: userData.name };
|
return { id: userData.id, name: userData.name }
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
|
|
||||||
return shared;
|
return shared
|
||||||
};
|
}
|
||||||
|
|
||||||
export default async function CodePage({ params }: { params: { id: string } }) {
|
export default async function CodePage({ params }: { params: { id: string } }) {
|
||||||
const user = await currentUser();
|
const user = await currentUser()
|
||||||
const sandboxId = params.id;
|
const sandboxId = params.id
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect("/");
|
redirect("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
const userData = await getUserData(user.id);
|
const userData = await getUserData(user.id)
|
||||||
const sandboxData = await getSandboxData(sandboxId);
|
const sandboxData = await getSandboxData(sandboxId)
|
||||||
const shared = await getSharedUsers(sandboxData.usersToSandboxes);
|
const shared = await getSharedUsers(sandboxData.usersToSandboxes)
|
||||||
|
|
||||||
const isOwner = sandboxData.userId === user.id;
|
const isOwner = sandboxData.userId === user.id
|
||||||
const isSharedUser = shared.some((uts) => uts.id === user.id);
|
const isSharedUser = shared.some((uts) => uts.id === user.id)
|
||||||
|
|
||||||
if (!isOwner && !isSharedUser) {
|
if (!isOwner && !isSharedUser) {
|
||||||
return notFound();
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("sandboxes: ", sandboxData);
|
console.log("sandboxes: ", sandboxData)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
||||||
<Room id={sandboxId}>
|
<Room id={sandboxId}>
|
||||||
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
|
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
|
||||||
<div className="w-screen flex grow">
|
<div className="w-screen flex grow">
|
||||||
<Editor
|
<Editor userData={userData} sandboxData={sandboxData} />
|
||||||
isOwner={isOwner}
|
|
||||||
userData={userData}
|
|
||||||
sandboxData={sandboxData}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Room>
|
</Room>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
import Image from "next/image";
|
import Image from "next/image"
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react"
|
||||||
import ProjectCardDropdown from "./dropdown";
|
import ProjectCardDropdown from "./dropdown"
|
||||||
import { Clock, Globe, Lock } from "lucide-react";
|
import { Clock, Globe, Lock } from "lucide-react"
|
||||||
import { Sandbox } from "@/lib/types";
|
import { Sandbox } from "@/lib/types"
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card"
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
export default function ProjectCard({
|
export default function ProjectCard({
|
||||||
children,
|
children,
|
||||||
@ -16,33 +16,33 @@ export default function ProjectCard({
|
|||||||
onDelete,
|
onDelete,
|
||||||
deletingId,
|
deletingId,
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode
|
||||||
sandbox: Sandbox;
|
sandbox: Sandbox
|
||||||
onVisibilityChange: (sandbox: Sandbox) => void;
|
onVisibilityChange: (sandbox: Sandbox) => void
|
||||||
onDelete: (sandbox: Sandbox) => void;
|
onDelete: (sandbox: Sandbox) => void
|
||||||
deletingId: string;
|
deletingId: string
|
||||||
}) {
|
}) {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false)
|
||||||
const [date, setDate] = useState<string>();
|
const [date, setDate] = useState<string>()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const createdAt = new Date(sandbox.createdAt);
|
const createdAt = new Date(sandbox.createdAt)
|
||||||
const now = new Date();
|
const now = new Date()
|
||||||
const diffInMinutes = Math.floor(
|
const diffInMinutes = Math.floor(
|
||||||
(now.getTime() - createdAt.getTime()) / 60000
|
(now.getTime() - createdAt.getTime()) / 60000
|
||||||
);
|
)
|
||||||
|
|
||||||
if (diffInMinutes < 1) {
|
if (diffInMinutes < 1) {
|
||||||
setDate("Now");
|
setDate("Now")
|
||||||
} else if (diffInMinutes < 60) {
|
} else if (diffInMinutes < 60) {
|
||||||
setDate(`${diffInMinutes}m ago`);
|
setDate(`${diffInMinutes}m ago`)
|
||||||
} else if (diffInMinutes < 1440) {
|
} else if (diffInMinutes < 1440) {
|
||||||
setDate(`${Math.floor(diffInMinutes / 60)}h ago`);
|
setDate(`${Math.floor(diffInMinutes / 60)}h ago`)
|
||||||
} else {
|
} else {
|
||||||
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`);
|
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
|
||||||
}
|
}
|
||||||
}, [sandbox]);
|
}, [sandbox])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -52,7 +52,7 @@ export default function ProjectCard({
|
|||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
className={`group/canvas-card p-4 h-48 flex flex-col justify-between items-start hover:border-muted-foreground/50 relative overflow-hidden transition-all`}
|
className={`group/canvas-card p-4 h-48 flex flex-col justify-between items-start hover:border-muted-foreground/50 relative overflow-hidden transition-all`}
|
||||||
>
|
>
|
||||||
{/* <AnimatePresence>
|
<AnimatePresence>
|
||||||
{hovered && (
|
{hovered && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
@ -62,7 +62,7 @@ export default function ProjectCard({
|
|||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence> */}
|
</AnimatePresence>
|
||||||
|
|
||||||
<div className="space-x-2 flex items-center justify-start w-full z-10">
|
<div className="space-x-2 flex items-center justify-start w-full z-10">
|
||||||
<Image
|
<Image
|
||||||
@ -101,5 +101,5 @@ export default function ProjectCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,110 +1,109 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { 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 { io } from "socket.io-client";
|
import { io } from "socket.io-client"
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner"
|
||||||
import { useClerk } from "@clerk/nextjs";
|
import { useClerk } from "@clerk/nextjs"
|
||||||
|
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs"
|
||||||
import LiveblocksProvider from "@liveblocks/yjs";
|
import LiveblocksProvider from "@liveblocks/yjs"
|
||||||
import { MonacoBinding } from "y-monaco";
|
import { MonacoBinding } from "y-monaco"
|
||||||
import { Awareness } from "y-protocols/awareness";
|
import { Awareness } from "y-protocols/awareness"
|
||||||
import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config";
|
import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable";
|
} from "@/components/ui/resizable"
|
||||||
import { FileJson, Loader2, TerminalSquare } from "lucide-react";
|
import { FileJson, Loader2, TerminalSquare } from "lucide-react"
|
||||||
import Tab from "../ui/tab";
|
import Tab from "../ui/tab"
|
||||||
import Sidebar from "./sidebar";
|
import Sidebar from "./sidebar"
|
||||||
import GenerateInput from "./generate";
|
import GenerateInput from "./generate"
|
||||||
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types";
|
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
|
||||||
import { addNew, processFileType, validateName } from "@/lib/utils";
|
import { addNew, processFileType, validateName } from "@/lib/utils"
|
||||||
import { Cursors } from "./live/cursors";
|
import { Cursors } from "./live/cursors"
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm"
|
||||||
import DisableAccessModal from "./live/disableModal";
|
import DisableAccessModal from "./live/disableModal"
|
||||||
import Loading from "./loading";
|
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"
|
||||||
|
|
||||||
export default function CodeEditor({
|
export default function CodeEditor({
|
||||||
userData,
|
userData,
|
||||||
sandboxData,
|
sandboxData,
|
||||||
ip,
|
ip,
|
||||||
}: {
|
}: {
|
||||||
userData: User;
|
userData: User
|
||||||
sandboxData: Sandbox;
|
sandboxData: Sandbox
|
||||||
ip: string;
|
ip: string
|
||||||
}) {
|
}) {
|
||||||
const socket = io(
|
const socket = io(
|
||||||
// `ws://${sandboxData.id}.ws.ishaand.com?userId=${userData.id}&sandboxId=${sandboxData.id}`
|
|
||||||
`http://${ip}:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`,
|
`http://${ip}:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`,
|
||||||
{
|
{
|
||||||
timeout: 2000,
|
timeout: 2000,
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true);
|
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
||||||
const [disableAccess, setDisableAccess] = useState({
|
const [disableAccess, setDisableAccess] = useState({
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
message: "",
|
message: "",
|
||||||
});
|
})
|
||||||
|
|
||||||
// File state
|
// File state
|
||||||
const [files, setFiles] = useState<(TFolder | TFile)[]>([]);
|
const [files, setFiles] = useState<(TFolder | TFile)[]>([])
|
||||||
const [tabs, setTabs] = useState<TTab[]>([]);
|
const [tabs, setTabs] = useState<TTab[]>([])
|
||||||
const [activeFileId, setActiveFileId] = useState<string>("");
|
const [activeFileId, setActiveFileId] = useState<string>("")
|
||||||
const [activeFileContent, setActiveFileContent] = useState("");
|
const [activeFileContent, setActiveFileContent] = useState("")
|
||||||
const [deletingFolderId, setDeletingFolderId] = useState("");
|
const [deletingFolderId, setDeletingFolderId] = useState("")
|
||||||
|
|
||||||
// Editor state
|
// Editor state
|
||||||
const [editorLanguage, setEditorLanguage] = useState("plaintext");
|
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
||||||
const [cursorLine, setCursorLine] = useState(0);
|
const [cursorLine, setCursorLine] = useState(0)
|
||||||
const [editorRef, setEditorRef] =
|
const [editorRef, setEditorRef] =
|
||||||
useState<monaco.editor.IStandaloneCodeEditor>();
|
useState<monaco.editor.IStandaloneCodeEditor>()
|
||||||
|
|
||||||
// AI Copilot state
|
// AI Copilot state
|
||||||
const [ai, setAi] = useState(false);
|
const [ai, setAi] = useState(false)
|
||||||
const [generate, setGenerate] = useState<{
|
const [generate, setGenerate] = useState<{
|
||||||
show: boolean;
|
show: boolean
|
||||||
id: string;
|
id: string
|
||||||
line: number;
|
line: number
|
||||||
widget: monaco.editor.IContentWidget | undefined;
|
widget: monaco.editor.IContentWidget | undefined
|
||||||
pref: monaco.editor.ContentWidgetPositionPreference[];
|
pref: monaco.editor.ContentWidgetPositionPreference[]
|
||||||
width: number;
|
width: number
|
||||||
}>({ show: false, line: 0, id: "", widget: undefined, pref: [], width: 0 });
|
}>({ show: false, line: 0, id: "", widget: undefined, pref: [], width: 0 })
|
||||||
const [decorations, setDecorations] = useState<{
|
const [decorations, setDecorations] = useState<{
|
||||||
options: monaco.editor.IModelDeltaDecoration[];
|
options: monaco.editor.IModelDeltaDecoration[]
|
||||||
instance: monaco.editor.IEditorDecorationsCollection | undefined;
|
instance: monaco.editor.IEditorDecorationsCollection | undefined
|
||||||
}>({ options: [], instance: undefined });
|
}>({ options: [], instance: undefined })
|
||||||
|
|
||||||
// Terminal state
|
// Terminal state
|
||||||
const [terminals, setTerminals] = useState<
|
const [terminals, setTerminals] = useState<
|
||||||
{
|
{
|
||||||
id: string;
|
id: string
|
||||||
terminal: Terminal | null;
|
terminal: Terminal | null
|
||||||
}[]
|
}[]
|
||||||
>([]);
|
>([])
|
||||||
|
|
||||||
const isOwner = sandboxData.userId === userData.id;
|
const isOwner = sandboxData.userId === userData.id
|
||||||
const clerk = useClerk();
|
const clerk = useClerk()
|
||||||
|
|
||||||
// Liveblocks hooks
|
// Liveblocks hooks
|
||||||
const room = useRoom();
|
const room = useRoom()
|
||||||
const [provider, setProvider] = useState<TypedLiveblocksProvider>();
|
const [provider, setProvider] = useState<TypedLiveblocksProvider>()
|
||||||
|
|
||||||
// Refs for libraries / features
|
// Refs for libraries / features
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
const editorContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const monacoRef = useRef<typeof monaco | null>(null);
|
const monacoRef = useRef<typeof monaco | null>(null)
|
||||||
const generateRef = useRef<HTMLDivElement>(null);
|
const generateRef = useRef<HTMLDivElement>(null)
|
||||||
const generateWidgetRef = useRef<HTMLDivElement>(null);
|
const generateWidgetRef = useRef<HTMLDivElement>(null)
|
||||||
const previewPanelRef = useRef<ImperativePanelHandle>(null);
|
const previewPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
const editorPanelRef = useRef<ImperativePanelHandle>(null);
|
const editorPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
|
|
||||||
// Pre-mount editor keybindings
|
// Pre-mount editor keybindings
|
||||||
const handleEditorWillMount: BeforeMount = (monaco) => {
|
const handleEditorWillMount: BeforeMount = (monaco) => {
|
||||||
@ -113,21 +112,21 @@ export default function CodeEditor({
|
|||||||
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG,
|
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG,
|
||||||
command: "null",
|
command: "null",
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
};
|
}
|
||||||
|
|
||||||
// Post-mount editor keybindings and actions
|
// Post-mount editor keybindings and actions
|
||||||
const handleEditorMount: OnMount = (editor, monaco) => {
|
const handleEditorMount: OnMount = (editor, monaco) => {
|
||||||
setEditorRef(editor);
|
setEditorRef(editor)
|
||||||
monacoRef.current = monaco;
|
monacoRef.current = monaco
|
||||||
|
|
||||||
editor.onDidChangeCursorPosition((e) => {
|
editor.onDidChangeCursorPosition((e) => {
|
||||||
const { column, lineNumber } = e.position;
|
const { column, lineNumber } = e.position
|
||||||
if (lineNumber === cursorLine) return;
|
if (lineNumber === cursorLine) return
|
||||||
setCursorLine(lineNumber);
|
setCursorLine(lineNumber)
|
||||||
|
|
||||||
const model = editor.getModel();
|
const model = editor.getModel()
|
||||||
const endColumn = model?.getLineContent(lineNumber).length || 0;
|
const endColumn = model?.getLineContent(lineNumber).length || 0
|
||||||
|
|
||||||
setDecorations((prev) => {
|
setDecorations((prev) => {
|
||||||
return {
|
return {
|
||||||
@ -145,18 +144,18 @@ export default function CodeEditor({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
editor.onDidBlurEditorText((e) => {
|
editor.onDidBlurEditorText((e) => {
|
||||||
setDecorations((prev) => {
|
setDecorations((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
options: [],
|
options: [],
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
editor.addAction({
|
editor.addAction({
|
||||||
id: "generate",
|
id: "generate",
|
||||||
@ -170,11 +169,11 @@ export default function CodeEditor({
|
|||||||
...prev,
|
...prev,
|
||||||
show: !prev.show,
|
show: !prev.show,
|
||||||
pref: [monaco.editor.ContentWidgetPositionPreference.BELOW],
|
pref: [monaco.editor.ContentWidgetPositionPreference.BELOW],
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
// Generate widget effect
|
// Generate widget effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -183,32 +182,32 @@ export default function CodeEditor({
|
|||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
show: false,
|
show: false,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (generate.show) {
|
if (generate.show) {
|
||||||
editorRef?.changeViewZones(function (changeAccessor) {
|
editorRef?.changeViewZones(function (changeAccessor) {
|
||||||
if (!generateRef.current) return;
|
if (!generateRef.current) return
|
||||||
const id = changeAccessor.addZone({
|
const id = changeAccessor.addZone({
|
||||||
afterLineNumber: cursorLine,
|
afterLineNumber: cursorLine,
|
||||||
heightInLines: 3,
|
heightInLines: 3,
|
||||||
domNode: generateRef.current,
|
domNode: generateRef.current,
|
||||||
});
|
})
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id, line: cursorLine };
|
return { ...prev, id, line: cursorLine }
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!generateWidgetRef.current) return;
|
if (!generateWidgetRef.current) return
|
||||||
const widgetElement = generateWidgetRef.current;
|
const widgetElement = generateWidgetRef.current
|
||||||
|
|
||||||
const contentWidget = {
|
const contentWidget = {
|
||||||
getDomNode: () => {
|
getDomNode: () => {
|
||||||
return widgetElement;
|
return widgetElement
|
||||||
},
|
},
|
||||||
getId: () => {
|
getId: () => {
|
||||||
return "generate.widget";
|
return "generate.widget"
|
||||||
},
|
},
|
||||||
getPosition: () => {
|
getPosition: () => {
|
||||||
return {
|
return {
|
||||||
@ -217,232 +216,232 @@ export default function CodeEditor({
|
|||||||
column: 1,
|
column: 1,
|
||||||
},
|
},
|
||||||
preference: generate.pref,
|
preference: generate.pref,
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
// window width - sidebar width, times the percentage of the editor panel
|
// window width - sidebar width, times the percentage of the editor panel
|
||||||
const width = editorPanelRef.current
|
const width = editorPanelRef.current
|
||||||
? (editorPanelRef.current.getSize() / 100) * (window.innerWidth - 224)
|
? (editorPanelRef.current.getSize() / 100) * (window.innerWidth - 224)
|
||||||
: 400; //fallback
|
: 400 //fallback
|
||||||
|
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
widget: contentWidget,
|
widget: contentWidget,
|
||||||
width,
|
width,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
editorRef?.addContentWidget(contentWidget);
|
editorRef?.addContentWidget(contentWidget)
|
||||||
|
|
||||||
if (generateRef.current && generateWidgetRef.current) {
|
if (generateRef.current && generateWidgetRef.current) {
|
||||||
editorRef?.applyFontInfo(generateRef.current);
|
editorRef?.applyFontInfo(generateRef.current)
|
||||||
editorRef?.applyFontInfo(generateWidgetRef.current);
|
editorRef?.applyFontInfo(generateWidgetRef.current)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
editorRef?.changeViewZones(function (changeAccessor) {
|
editorRef?.changeViewZones(function (changeAccessor) {
|
||||||
changeAccessor.removeZone(generate.id);
|
changeAccessor.removeZone(generate.id)
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id: "" };
|
return { ...prev, id: "" }
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!generate.widget) return;
|
if (!generate.widget) return
|
||||||
editorRef?.removeContentWidget(generate.widget);
|
editorRef?.removeContentWidget(generate.widget)
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
widget: undefined,
|
widget: undefined,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}, [generate.show]);
|
}, [generate.show])
|
||||||
|
|
||||||
// Decorations effect for generate widget tips
|
// Decorations effect for generate widget tips
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (decorations.options.length === 0) {
|
if (decorations.options.length === 0) {
|
||||||
decorations.instance?.clear();
|
decorations.instance?.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ai) return;
|
if (!ai) return
|
||||||
|
|
||||||
if (decorations.instance) {
|
if (decorations.instance) {
|
||||||
decorations.instance.set(decorations.options);
|
decorations.instance.set(decorations.options)
|
||||||
} else {
|
} else {
|
||||||
const instance = editorRef?.createDecorationsCollection();
|
const instance = editorRef?.createDecorationsCollection()
|
||||||
instance?.set(decorations.options);
|
instance?.set(decorations.options)
|
||||||
|
|
||||||
setDecorations((prev) => {
|
setDecorations((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
instance,
|
instance,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}, [decorations.options]);
|
}, [decorations.options])
|
||||||
|
|
||||||
// Save file keybinding logic effect
|
// Save file keybinding logic effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) =>
|
prev.map((tab) =>
|
||||||
tab.id === activeFileId ? { ...tab, saved: true } : tab
|
tab.id === activeFileId ? { ...tab, saved: true } : tab
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
|
|
||||||
socket.emit("saveFile", activeFileId, editorRef?.getValue());
|
socket.emit("saveFile", activeFileId, editorRef?.getValue())
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
document.addEventListener("keydown", down);
|
document.addEventListener("keydown", down)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", down);
|
document.removeEventListener("keydown", down)
|
||||||
};
|
}
|
||||||
}, [tabs, activeFileId]);
|
}, [tabs, activeFileId])
|
||||||
|
|
||||||
// Liveblocks live collaboration setup effect
|
// Liveblocks live collaboration setup effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tab = tabs.find((t) => t.id === activeFileId);
|
const tab = tabs.find((t) => t.id === activeFileId)
|
||||||
const model = editorRef?.getModel();
|
const model = editorRef?.getModel()
|
||||||
|
|
||||||
if (!editorRef || !tab || !model) return;
|
if (!editorRef || !tab || !model) return
|
||||||
|
|
||||||
const yDoc = new Y.Doc();
|
const yDoc = new Y.Doc()
|
||||||
const yText = yDoc.getText(tab.id);
|
const yText = yDoc.getText(tab.id)
|
||||||
const yProvider: any = new LiveblocksProvider(room, yDoc);
|
const yProvider: any = new LiveblocksProvider(room, yDoc)
|
||||||
|
|
||||||
const onSync = (isSynced: boolean) => {
|
const onSync = (isSynced: boolean) => {
|
||||||
if (isSynced) {
|
if (isSynced) {
|
||||||
const text = yText.toString();
|
const text = yText.toString()
|
||||||
if (text === "") {
|
if (text === "") {
|
||||||
if (activeFileContent) {
|
if (activeFileContent) {
|
||||||
yText.insert(0, activeFileContent);
|
yText.insert(0, activeFileContent)
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
yText.insert(0, editorRef.getValue());
|
yText.insert(0, editorRef.getValue())
|
||||||
}, 0);
|
}, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
yProvider.on("sync", onSync);
|
yProvider.on("sync", onSync)
|
||||||
|
|
||||||
setProvider(yProvider);
|
setProvider(yProvider)
|
||||||
|
|
||||||
const binding = new MonacoBinding(
|
const binding = new MonacoBinding(
|
||||||
yText,
|
yText,
|
||||||
model,
|
model,
|
||||||
new Set([editorRef]),
|
new Set([editorRef]),
|
||||||
yProvider.awareness as Awareness
|
yProvider.awareness as Awareness
|
||||||
);
|
)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
yDoc.destroy();
|
yDoc.destroy()
|
||||||
yProvider.destroy();
|
yProvider.destroy()
|
||||||
binding.destroy();
|
binding.destroy()
|
||||||
yProvider.off("sync", onSync);
|
yProvider.off("sync", onSync)
|
||||||
};
|
}
|
||||||
}, [editorRef, room, activeFileContent]);
|
}, [editorRef, room, activeFileContent])
|
||||||
|
|
||||||
// Connection/disconnection effect
|
// Connection/disconnection effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.connect();
|
socket.connect()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.disconnect();
|
socket.disconnect()
|
||||||
};
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
// Socket event listener effect
|
// Socket event listener effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onConnect = () => {};
|
const onConnect = () => {}
|
||||||
|
|
||||||
const onDisconnect = () => {
|
const onDisconnect = () => {
|
||||||
setTerminals([]);
|
setTerminals([])
|
||||||
};
|
}
|
||||||
|
|
||||||
const onLoadedEvent = (files: (TFolder | TFile)[]) => {
|
const onLoadedEvent = (files: (TFolder | TFile)[]) => {
|
||||||
setFiles(files);
|
setFiles(files)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onRateLimit = (message: string) => {
|
const onRateLimit = (message: string) => {
|
||||||
toast.error(message);
|
toast.error(message)
|
||||||
};
|
}
|
||||||
|
|
||||||
const onTerminalResponse = (response: { id: string; data: string }) => {
|
const onTerminalResponse = (response: { id: string; data: string }) => {
|
||||||
const term = terminals.find((t) => t.id === response.id);
|
const term = terminals.find((t) => t.id === response.id)
|
||||||
if (term && term.terminal) {
|
if (term && term.terminal) {
|
||||||
term.terminal.write(response.data);
|
term.terminal.write(response.data)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const onDisableAccess = (message: string) => {
|
const onDisableAccess = (message: string) => {
|
||||||
if (!isOwner)
|
if (!isOwner)
|
||||||
setDisableAccess({
|
setDisableAccess({
|
||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
message,
|
message,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
socket.on("connect", onConnect);
|
socket.on("connect", onConnect)
|
||||||
socket.on("disconnect", onDisconnect);
|
socket.on("disconnect", onDisconnect)
|
||||||
socket.on("loaded", onLoadedEvent);
|
socket.on("loaded", onLoadedEvent)
|
||||||
socket.on("rateLimit", onRateLimit);
|
socket.on("rateLimit", onRateLimit)
|
||||||
socket.on("terminalResponse", onTerminalResponse);
|
socket.on("terminalResponse", onTerminalResponse)
|
||||||
socket.on("disableAccess", onDisableAccess);
|
socket.on("disableAccess", onDisableAccess)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("connect", onConnect);
|
socket.off("connect", onConnect)
|
||||||
socket.off("disconnect", onDisconnect);
|
socket.off("disconnect", onDisconnect)
|
||||||
socket.off("loaded", onLoadedEvent);
|
socket.off("loaded", onLoadedEvent)
|
||||||
socket.off("rateLimit", onRateLimit);
|
socket.off("rateLimit", onRateLimit)
|
||||||
socket.off("terminalResponse", onTerminalResponse);
|
socket.off("terminalResponse", onTerminalResponse)
|
||||||
socket.off("disableAccess", onDisableAccess);
|
socket.off("disableAccess", onDisableAccess)
|
||||||
};
|
}
|
||||||
// }, []);
|
// }, []);
|
||||||
}, [terminals]);
|
}, [terminals])
|
||||||
|
|
||||||
// Helper functions for tabs:
|
// Helper functions for tabs:
|
||||||
|
|
||||||
// Select file and load content
|
// Select file and load content
|
||||||
const selectFile = (tab: TTab) => {
|
const selectFile = (tab: TTab) => {
|
||||||
if (tab.id === activeFileId) return;
|
if (tab.id === activeFileId) return
|
||||||
|
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
show: false,
|
show: false,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
const exists = tabs.find((t) => t.id === tab.id);
|
const exists = tabs.find((t) => t.id === tab.id)
|
||||||
|
|
||||||
setTabs((prev) => {
|
setTabs((prev) => {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
setActiveFileId(exists.id);
|
setActiveFileId(exists.id)
|
||||||
return prev;
|
return prev
|
||||||
}
|
}
|
||||||
return [...prev, tab];
|
return [...prev, tab]
|
||||||
});
|
})
|
||||||
|
|
||||||
socket.emit("getFile", tab.id, (response: string) => {
|
socket.emit("getFile", tab.id, (response: string) => {
|
||||||
setActiveFileContent(response);
|
setActiveFileContent(response)
|
||||||
});
|
})
|
||||||
setEditorLanguage(processFileType(tab.name));
|
setEditorLanguage(processFileType(tab.name))
|
||||||
setActiveFileId(tab.id);
|
setActiveFileId(tab.id)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Close tab and remove from tabs
|
// Close tab and remove from tabs
|
||||||
const closeTab = (id: string) => {
|
const closeTab = (id: string) => {
|
||||||
const numTabs = tabs.length;
|
const numTabs = tabs.length
|
||||||
const index = tabs.findIndex((t) => t.id === id);
|
const index = tabs.findIndex((t) => t.id === id)
|
||||||
|
|
||||||
console.log("closing tab", id, index);
|
console.log("closing tab", id, index)
|
||||||
|
|
||||||
if (index === -1) return;
|
if (index === -1) return
|
||||||
|
|
||||||
const nextId =
|
const nextId =
|
||||||
activeFileId === id
|
activeFileId === id
|
||||||
@ -451,49 +450,49 @@ export default function CodeEditor({
|
|||||||
: index < numTabs - 1
|
: index < numTabs - 1
|
||||||
? tabs[index + 1].id
|
? tabs[index + 1].id
|
||||||
: tabs[index - 1].id
|
: tabs[index - 1].id
|
||||||
: activeFileId;
|
: activeFileId
|
||||||
|
|
||||||
setTabs((prev) => prev.filter((t) => t.id !== id));
|
setTabs((prev) => prev.filter((t) => t.id !== id))
|
||||||
|
|
||||||
if (!nextId) {
|
if (!nextId) {
|
||||||
setActiveFileId("");
|
setActiveFileId("")
|
||||||
} else {
|
} else {
|
||||||
const nextTab = tabs.find((t) => t.id === nextId);
|
const nextTab = tabs.find((t) => t.id === nextId)
|
||||||
if (nextTab) {
|
if (nextTab) {
|
||||||
selectFile(nextTab);
|
selectFile(nextTab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const closeTabs = (ids: string[]) => {
|
const closeTabs = (ids: string[]) => {
|
||||||
const numTabs = tabs.length;
|
const numTabs = tabs.length
|
||||||
|
|
||||||
if (numTabs === 0) return;
|
if (numTabs === 0) return
|
||||||
|
|
||||||
const allIndexes = ids.map((id) => tabs.findIndex((t) => t.id === id));
|
const allIndexes = ids.map((id) => tabs.findIndex((t) => t.id === id))
|
||||||
|
|
||||||
const indexes = allIndexes.filter((index) => index !== -1);
|
const indexes = allIndexes.filter((index) => index !== -1)
|
||||||
if (indexes.length === 0) return;
|
if (indexes.length === 0) return
|
||||||
|
|
||||||
console.log("closing tabs", ids, indexes);
|
console.log("closing tabs", ids, indexes)
|
||||||
|
|
||||||
const activeIndex = tabs.findIndex((t) => t.id === activeFileId);
|
const activeIndex = tabs.findIndex((t) => t.id === activeFileId)
|
||||||
|
|
||||||
const newTabs = tabs.filter((t) => !ids.includes(t.id));
|
const newTabs = tabs.filter((t) => !ids.includes(t.id))
|
||||||
setTabs(newTabs);
|
setTabs(newTabs)
|
||||||
|
|
||||||
if (indexes.length === numTabs) {
|
if (indexes.length === numTabs) {
|
||||||
setActiveFileId("");
|
setActiveFileId("")
|
||||||
} else {
|
} else {
|
||||||
const nextTab =
|
const nextTab =
|
||||||
newTabs.length > activeIndex
|
newTabs.length > activeIndex
|
||||||
? newTabs[activeIndex]
|
? newTabs[activeIndex]
|
||||||
: newTabs[newTabs.length - 1];
|
: newTabs[newTabs.length - 1]
|
||||||
if (nextTab) {
|
if (nextTab) {
|
||||||
selectFile(nextTab);
|
selectFile(nextTab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleRename = (
|
const handleRename = (
|
||||||
id: string,
|
id: string,
|
||||||
@ -501,40 +500,40 @@ export default function CodeEditor({
|
|||||||
oldName: string,
|
oldName: string,
|
||||||
type: "file" | "folder"
|
type: "file" | "folder"
|
||||||
) => {
|
) => {
|
||||||
const valid = validateName(newName, oldName, type);
|
const valid = validateName(newName, oldName, type)
|
||||||
if (!valid.status) {
|
if (!valid.status) {
|
||||||
if (valid.message) toast.error("Invalid file name.");
|
if (valid.message) toast.error("Invalid file name.")
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.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))
|
||||||
);
|
)
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDeleteFile = (file: TFile) => {
|
const handleDeleteFile = (file: TFile) => {
|
||||||
socket.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)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDeleteFolder = (folder: TFolder) => {
|
const handleDeleteFolder = (folder: TFolder) => {
|
||||||
setDeletingFolderId(folder.id);
|
setDeletingFolderId(folder.id)
|
||||||
console.log("deleting folder", folder.id);
|
console.log("deleting folder", folder.id)
|
||||||
|
|
||||||
socket.emit("getFolder", folder.id, (response: string[]) =>
|
socket.emit("getFolder", folder.id, (response: string[]) =>
|
||||||
closeTabs(response)
|
closeTabs(response)
|
||||||
);
|
)
|
||||||
|
|
||||||
socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
|
socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
|
||||||
setFiles(response);
|
setFiles(response)
|
||||||
setDeletingFolderId("");
|
setDeletingFolderId("")
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
// On disabled access for shared users, show un-interactable loading placeholder + info modal
|
// On disabled access for shared users, show un-interactable loading placeholder + info modal
|
||||||
if (disableAccess.isDisabled)
|
if (disableAccess.isDisabled)
|
||||||
@ -547,7 +546,7 @@ export default function CodeEditor({
|
|||||||
/>
|
/>
|
||||||
<Loading />
|
<Loading />
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -569,41 +568,41 @@ export default function CodeEditor({
|
|||||||
}}
|
}}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
editorRef?.changeViewZones(function (changeAccessor) {
|
editorRef?.changeViewZones(function (changeAccessor) {
|
||||||
changeAccessor.removeZone(generate.id);
|
changeAccessor.removeZone(generate.id)
|
||||||
|
|
||||||
if (!generateRef.current) return;
|
if (!generateRef.current) return
|
||||||
const id = changeAccessor.addZone({
|
const id = changeAccessor.addZone({
|
||||||
afterLineNumber: cursorLine,
|
afterLineNumber: cursorLine,
|
||||||
heightInLines: 12,
|
heightInLines: 12,
|
||||||
domNode: generateRef.current,
|
domNode: generateRef.current,
|
||||||
});
|
})
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id };
|
return { ...prev, id }
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
onAccept={(code: string) => {
|
onAccept={(code: string) => {
|
||||||
const line = generate.line;
|
const line = generate.line
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
show: !prev.show,
|
show: !prev.show,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
const file = editorRef?.getValue();
|
const file = editorRef?.getValue()
|
||||||
|
|
||||||
const lines = file?.split("\n") || [];
|
const lines = file?.split("\n") || []
|
||||||
lines.splice(line - 1, 0, code);
|
lines.splice(line - 1, 0, code)
|
||||||
const updatedFile = lines.join("\n");
|
const updatedFile = lines.join("\n")
|
||||||
editorRef?.setValue(updatedFile);
|
editorRef?.setValue(updatedFile)
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
show: !prev.show,
|
show: !prev.show,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@ -643,7 +642,7 @@ export default function CodeEditor({
|
|||||||
saved={tab.saved}
|
saved={tab.saved}
|
||||||
selected={activeFileId === tab.id}
|
selected={activeFileId === tab.id}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
selectFile(tab);
|
selectFile(tab)
|
||||||
}}
|
}}
|
||||||
onClose={() => closeTab(tab.id)}
|
onClose={() => closeTab(tab.id)}
|
||||||
>
|
>
|
||||||
@ -680,7 +679,7 @@ export default function CodeEditor({
|
|||||||
? { ...tab, saved: true }
|
? { ...tab, saved: true }
|
||||||
: tab
|
: tab
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) =>
|
prev.map((tab) =>
|
||||||
@ -688,7 +687,7 @@ export default function CodeEditor({
|
|||||||
? { ...tab, saved: false }
|
? { ...tab, saved: false }
|
||||||
: tab
|
: tab
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={{
|
options={{
|
||||||
@ -732,8 +731,8 @@ export default function CodeEditor({
|
|||||||
<PreviewWindow
|
<PreviewWindow
|
||||||
collapsed={isPreviewCollapsed}
|
collapsed={isPreviewCollapsed}
|
||||||
open={() => {
|
open={() => {
|
||||||
previewPanelRef.current?.expand();
|
previewPanelRef.current?.expand()
|
||||||
setIsPreviewCollapsed(false);
|
setIsPreviewCollapsed(false)
|
||||||
}}
|
}}
|
||||||
ip={ip}
|
ip={ip}
|
||||||
/>
|
/>
|
||||||
@ -761,5 +760,5 @@ export default function CodeEditor({
|
|||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button"
|
||||||
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react";
|
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client"
|
||||||
import { Editor } from "@monaco-editor/react";
|
import { Editor } from "@monaco-editor/react"
|
||||||
import { User } from "@/lib/types";
|
import { User } from "@/lib/types"
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner"
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation"
|
||||||
// import monaco from "monaco-editor"
|
// import monaco from "monaco-editor"
|
||||||
|
|
||||||
export default function GenerateInput({
|
export default function GenerateInput({
|
||||||
@ -20,54 +20,54 @@ export default function GenerateInput({
|
|||||||
onAccept,
|
onAccept,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
user: User;
|
user: User
|
||||||
socket: Socket;
|
socket: Socket
|
||||||
width: number;
|
width: number
|
||||||
data: {
|
data: {
|
||||||
fileName: string;
|
fileName: string
|
||||||
code: string;
|
code: string
|
||||||
line: number;
|
line: number
|
||||||
};
|
}
|
||||||
editor: {
|
editor: {
|
||||||
language: string;
|
language: string
|
||||||
};
|
}
|
||||||
onExpand: () => void;
|
onExpand: () => void
|
||||||
onAccept: (code: string) => void;
|
onAccept: (code: string) => void
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("")
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [loading, setLoading] = useState({
|
const [loading, setLoading] = useState({
|
||||||
generate: false,
|
generate: false,
|
||||||
regenerate: false,
|
regenerate: false,
|
||||||
});
|
})
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("")
|
||||||
const [currentPrompt, setCurrentPrompt] = useState("");
|
const [currentPrompt, setCurrentPrompt] = useState("")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus()
|
||||||
}, 100);
|
}, 100)
|
||||||
}, [inputRef.current]);
|
}, [inputRef.current])
|
||||||
|
|
||||||
const handleGenerate = async ({
|
const handleGenerate = async ({
|
||||||
regenerate = false,
|
regenerate = false,
|
||||||
}: {
|
}: {
|
||||||
regenerate?: boolean;
|
regenerate?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
if (user.generations >= 10) {
|
if (user.generations >= 10) {
|
||||||
toast.error(
|
toast.error(
|
||||||
"You reached the maximum # of generations. Contact @ishaandey_ on X/Twitter to reset :)"
|
"You reached the maximum # of generations. Contact @ishaandey_ on X/Twitter to reset :)"
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading({ generate: !regenerate, regenerate });
|
setLoading({ generate: !regenerate, regenerate })
|
||||||
setCurrentPrompt(input);
|
setCurrentPrompt(input)
|
||||||
socket.emit(
|
socket.emit(
|
||||||
"generateCode",
|
"generateCode",
|
||||||
data.fileName,
|
data.fileName,
|
||||||
@ -75,25 +75,25 @@ export default function GenerateInput({
|
|||||||
data.line,
|
data.line,
|
||||||
regenerate ? currentPrompt : input,
|
regenerate ? currentPrompt : input,
|
||||||
(res: { response: string; success: boolean }) => {
|
(res: { response: string; success: boolean }) => {
|
||||||
console.log("Generated code", res.response, res.success);
|
console.log("Generated code", res.response, res.success)
|
||||||
// if (!res.success) {
|
// if (!res.success) {
|
||||||
// toast.error("Failed to generate code.");
|
// toast.error("Failed to generate code.");
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
setCode(res.response);
|
setCode(res.response)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (code) {
|
if (code) {
|
||||||
setExpanded(true);
|
setExpanded(true)
|
||||||
onExpand();
|
onExpand()
|
||||||
setLoading({ generate: false, regenerate: false });
|
setLoading({ generate: false, regenerate: false })
|
||||||
}
|
}
|
||||||
}, [code]);
|
}, [code])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full pr-4 space-y-2">
|
<div className="w-full pr-4 space-y-2">
|
||||||
@ -105,7 +105,7 @@ export default function GenerateInput({
|
|||||||
}}
|
}}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder="✨ Generate code with a prompt"
|
placeholder="Generate code with a prompt"
|
||||||
className="h-8 w-full rounded-md border border-muted-foreground bg-transparent px-3 py-1 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
className="h-8 w-full rounded-md border border-muted-foreground bg-transparent px-3 py-1 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -192,5 +192,5 @@ export default function GenerateInput({
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,68 +1,51 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic"
|
||||||
import Loading from "@/components/editor/loading";
|
import Loading from "@/components/editor/loading"
|
||||||
import { Sandbox, User } from "@/lib/types";
|
import { Sandbox, User } from "@/lib/types"
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react"
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner"
|
||||||
import { getTaskIp, startServer } from "@/lib/actions";
|
import { getTaskIp, startServer } from "@/lib/actions"
|
||||||
import { checkServiceStatus } from "@/lib/utils";
|
import { checkServiceStatus, setupServer } from "@/lib/utils"
|
||||||
|
|
||||||
const CodeEditor = dynamic(() => import("@/components/editor/editor"), {
|
const CodeEditor = dynamic(() => import("@/components/editor/editor"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => <Loading />,
|
loading: () => <Loading />,
|
||||||
});
|
})
|
||||||
|
|
||||||
export default function Editor({
|
export default function Editor({
|
||||||
isOwner,
|
|
||||||
userData,
|
userData,
|
||||||
sandboxData,
|
sandboxData,
|
||||||
}: {
|
}: {
|
||||||
isOwner: boolean;
|
userData: User
|
||||||
userData: User;
|
sandboxData: Sandbox
|
||||||
sandboxData: Sandbox;
|
|
||||||
}) {
|
}) {
|
||||||
const [isServiceRunning, setIsServiceRunning] = useState(false);
|
const isDev = process.env.VERCEL_ENV === "development"
|
||||||
const [isDeploymentActive, setIsDeploymentActive] = useState(false);
|
|
||||||
const [taskIp, setTaskIp] = useState<string>();
|
const [isServiceRunning, setIsServiceRunning] = useState(false)
|
||||||
const [didFail, setDidFail] = useState(false);
|
const [isDeploymentActive, setIsDeploymentActive] = useState(false)
|
||||||
|
const [taskIp, setTaskIp] = useState<string>()
|
||||||
|
const [didFail, setDidFail] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOwner) {
|
if (isDev) {
|
||||||
toast.error("You are not the owner of this sandbox. (TEMPORARY)");
|
setIsServiceRunning(true)
|
||||||
setDidFail(true);
|
setIsDeploymentActive(true)
|
||||||
return;
|
setTaskIp("localhost")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
startServer(sandboxData.id).then((response) => {
|
setupServer({
|
||||||
if (!response.success) {
|
sandboxId: sandboxData.id,
|
||||||
toast.error(response.message);
|
setIsServiceRunning,
|
||||||
setDidFail(true);
|
setIsDeploymentActive,
|
||||||
} else {
|
setTaskIp,
|
||||||
setIsServiceRunning(true);
|
setDidFail,
|
||||||
|
toast,
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
checkServiceStatus(sandboxData.id)
|
if (didFail) return <Loading didFail={didFail} />
|
||||||
.then(() => {
|
|
||||||
setIsDeploymentActive(true);
|
|
||||||
|
|
||||||
getTaskIp(sandboxData.id)
|
|
||||||
.then((ip) => {
|
|
||||||
setTaskIp(ip);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setDidFail(true);
|
|
||||||
toast.error("An error occurred while getting your server IP.");
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("An error occurred while initializing your server.");
|
|
||||||
setDidFail(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (didFail) return <Loading didFail={didFail} />;
|
|
||||||
if (!isServiceRunning || !isDeploymentActive || !taskIp)
|
if (!isServiceRunning || !isDeploymentActive || !taskIp)
|
||||||
return (
|
return (
|
||||||
<Loading
|
<Loading
|
||||||
@ -75,9 +58,9 @@ export default function Editor({
|
|||||||
: "Requesting your server creation..."
|
: "Requesting your server creation..."
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeEditor ip={taskIp} userData={userData} sandboxData={sandboxData} />
|
<CodeEditor ip={taskIp} userData={userData} sandboxData={sandboxData} />
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
"use server";
|
"use server"
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache"
|
||||||
import ecsClient, { ec2Client } from "./ecs";
|
import ecsClient, { ec2Client } from "./ecs"
|
||||||
import {
|
import {
|
||||||
CreateServiceCommand,
|
CreateServiceCommand,
|
||||||
DescribeClustersCommand,
|
DescribeClustersCommand,
|
||||||
DescribeServicesCommand,
|
DescribeServicesCommand,
|
||||||
DescribeTasksCommand,
|
DescribeTasksCommand,
|
||||||
ListTasksCommand,
|
ListTasksCommand,
|
||||||
} from "@aws-sdk/client-ecs";
|
} from "@aws-sdk/client-ecs"
|
||||||
import { DescribeNetworkInterfacesCommand } from "@aws-sdk/client-ec2";
|
import { DescribeNetworkInterfacesCommand } from "@aws-sdk/client-ec2"
|
||||||
|
|
||||||
export async function createSandbox(body: {
|
export async function createSandbox(body: {
|
||||||
type: string;
|
type: string
|
||||||
name: string;
|
name: string
|
||||||
userId: string;
|
userId: string
|
||||||
visibility: string;
|
visibility: string
|
||||||
}) {
|
}) {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
"https://database.ishaan1013.workers.dev/api/sandbox",
|
"https://database.ishaan1013.workers.dev/api/sandbox",
|
||||||
@ -26,15 +26,15 @@ export async function createSandbox(body: {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
return await res.text();
|
return await res.text()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSandbox(body: {
|
export async function updateSandbox(body: {
|
||||||
id: string;
|
id: string
|
||||||
name?: string;
|
name?: string
|
||||||
visibility?: "public" | "private";
|
visibility?: "public" | "private"
|
||||||
}) {
|
}) {
|
||||||
await fetch("https://database.ishaan1013.workers.dev/api/sandbox", {
|
await fetch("https://database.ishaan1013.workers.dev/api/sandbox", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -42,17 +42,17 @@ export async function updateSandbox(body: {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
})
|
||||||
|
|
||||||
revalidatePath("/dashboard");
|
revalidatePath("/dashboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSandbox(id: string) {
|
export async function deleteSandbox(id: string) {
|
||||||
await fetch(`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`, {
|
await fetch(`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
})
|
||||||
|
|
||||||
revalidatePath("/dashboard");
|
revalidatePath("/dashboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shareSandbox(sandboxId: string, email: string) {
|
export async function shareSandbox(sandboxId: string, email: string) {
|
||||||
@ -65,15 +65,15 @@ export async function shareSandbox(sandboxId: string, email: string) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ sandboxId, email }),
|
body: JSON.stringify({ sandboxId, email }),
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
const text = await res.text();
|
const text = await res.text()
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
return { success: false, message: text };
|
return { success: false, message: text }
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath(`/code/${sandboxId}`);
|
revalidatePath(`/code/${sandboxId}`)
|
||||||
return { success: true, message: "Shared successfully." };
|
return { success: true, message: "Shared successfully." }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unshareSandbox(sandboxId: string, userId: string) {
|
export async function unshareSandbox(sandboxId: string, userId: string) {
|
||||||
@ -83,102 +83,102 @@ export async function unshareSandbox(sandboxId: string, userId: string) {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ sandboxId, userId }),
|
body: JSON.stringify({ sandboxId, userId }),
|
||||||
});
|
})
|
||||||
|
|
||||||
revalidatePath(`/code/${sandboxId}`);
|
revalidatePath(`/code/${sandboxId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function describeService(serviceName: string) {
|
export async function describeService(serviceName: string) {
|
||||||
const command = new DescribeServicesCommand({
|
const command = new DescribeServicesCommand({
|
||||||
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
|
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
|
||||||
services: [serviceName],
|
services: [serviceName],
|
||||||
});
|
})
|
||||||
|
|
||||||
return await ecsClient.send(command);
|
return await ecsClient.send(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTaskIp(serviceName: string) {
|
export async function getTaskIp(serviceName: string) {
|
||||||
const listCommand = new ListTasksCommand({
|
const listCommand = new ListTasksCommand({
|
||||||
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
|
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
|
||||||
serviceName,
|
serviceName,
|
||||||
});
|
})
|
||||||
|
|
||||||
const listResponse = await ecsClient.send(listCommand);
|
const listResponse = await ecsClient.send(listCommand)
|
||||||
const taskArns = listResponse.taskArns;
|
const taskArns = listResponse.taskArns
|
||||||
|
|
||||||
const describeCommand = new DescribeTasksCommand({
|
const describeCommand = new DescribeTasksCommand({
|
||||||
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
|
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
|
||||||
tasks: taskArns,
|
tasks: taskArns,
|
||||||
});
|
})
|
||||||
|
|
||||||
const describeResponse = await ecsClient.send(describeCommand);
|
const describeResponse = await ecsClient.send(describeCommand)
|
||||||
const tasks = describeResponse.tasks;
|
const tasks = describeResponse.tasks
|
||||||
const taskAttachment = tasks?.[0].attachments?.[0].details;
|
const taskAttachment = tasks?.[0].attachments?.[0].details
|
||||||
if (!taskAttachment) {
|
if (!taskAttachment) {
|
||||||
throw new Error("Task attachment not found");
|
throw new Error("Task attachment not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const eni = taskAttachment.find(
|
const eni = taskAttachment.find(
|
||||||
(detail) => detail.name === "networkInterfaceId"
|
(detail) => detail.name === "networkInterfaceId"
|
||||||
)?.value;
|
)?.value
|
||||||
if (!eni) {
|
if (!eni) {
|
||||||
throw new Error("Network interface not found");
|
throw new Error("Network interface not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const describeNetworkInterfacesCommand = new DescribeNetworkInterfacesCommand(
|
const describeNetworkInterfacesCommand = new DescribeNetworkInterfacesCommand(
|
||||||
{
|
{
|
||||||
NetworkInterfaceIds: [eni],
|
NetworkInterfaceIds: [eni],
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
const describeNetworkInterfacesResponse = await ec2Client.send(
|
const describeNetworkInterfacesResponse = await ec2Client.send(
|
||||||
describeNetworkInterfacesCommand
|
describeNetworkInterfacesCommand
|
||||||
);
|
)
|
||||||
|
|
||||||
const ip =
|
const ip =
|
||||||
describeNetworkInterfacesResponse.NetworkInterfaces?.[0].Association
|
describeNetworkInterfacesResponse.NetworkInterfaces?.[0].Association
|
||||||
?.PublicIp;
|
?.PublicIp
|
||||||
if (!ip) {
|
if (!ip) {
|
||||||
throw new Error("Public IP not found");
|
throw new Error("Public IP not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ip;
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doesServiceExist(serviceName: string) {
|
export async function doesServiceExist(serviceName: string) {
|
||||||
const response = await describeService(serviceName);
|
const response = await describeService(serviceName)
|
||||||
const activeServices = response.services?.filter(
|
const activeServices = response.services?.filter(
|
||||||
(service) => service.status === "ACTIVE"
|
(service) => service.status === "ACTIVE"
|
||||||
);
|
)
|
||||||
|
|
||||||
console.log("activeServices: ", activeServices);
|
console.log("activeServices: ", activeServices)
|
||||||
|
|
||||||
return activeServices?.length === 1;
|
return activeServices?.length === 1
|
||||||
}
|
}
|
||||||
|
|
||||||
async function countServices() {
|
async function countServices() {
|
||||||
const command = new DescribeClustersCommand({
|
const command = new DescribeClustersCommand({
|
||||||
clusters: [process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!],
|
clusters: [process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!],
|
||||||
});
|
})
|
||||||
|
|
||||||
const response = await ecsClient.send(command);
|
const response = await ecsClient.send(command)
|
||||||
return response.clusters?.[0].activeServicesCount!;
|
return response.clusters?.[0].activeServicesCount!
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startServer(
|
export async function startServer(
|
||||||
serviceName: string
|
serviceName: string
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
const serviceExists = await doesServiceExist(serviceName);
|
const serviceExists = await doesServiceExist(serviceName)
|
||||||
if (serviceExists) {
|
if (serviceExists) {
|
||||||
return { success: true, message: "" };
|
return { success: true, message: "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeServices = await countServices();
|
const activeServices = await countServices()
|
||||||
if (activeServices >= 100) {
|
if (activeServices >= 100) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message:
|
message:
|
||||||
"Too many servers are running! Please try again later or contact @ishaandey_ on Twitter/X.",
|
"Too many servers are running! Please try again later or contact @ishaandey_ on Twitter/X.",
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = new CreateServiceCommand({
|
const command = new CreateServiceCommand({
|
||||||
@ -201,13 +201,13 @@ export async function startServer(
|
|||||||
assignPublicIp: "ENABLED",
|
assignPublicIp: "ENABLED",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await ecsClient.send(command);
|
const response = await ecsClient.send(command)
|
||||||
console.log("started server:", response.service?.serviceName);
|
console.log("started server:", response.service?.serviceName)
|
||||||
|
|
||||||
return { success: true, message: "" };
|
return { success: true, message: "" }
|
||||||
|
|
||||||
// store in workers kv:
|
// store in workers kv:
|
||||||
// {
|
// {
|
||||||
@ -223,6 +223,6 @@ export async function startServer(
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `Error starting server: ${error.message}. Try again in a minute, or contact @ishaandey_ on Twitter/X if it still doesn't work.`,
|
message: `Error starting server: ${error.message}. Try again in a minute, or contact @ishaandey_ on Twitter/X if it still doesn't work.`,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
import { type ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from "clsx"
|
||||||
// import { toast } from "sonner"
|
// import { toast } from "sonner"
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge"
|
||||||
import { Sandbox, TFile, TFolder } from "./types";
|
import { Sandbox, TFile, TFolder } from "./types"
|
||||||
import { Service } from "@aws-sdk/client-ecs";
|
import { Service } from "@aws-sdk/client-ecs"
|
||||||
import { describeService } from "./actions";
|
import {
|
||||||
|
describeService,
|
||||||
|
doesServiceExist,
|
||||||
|
getTaskIp,
|
||||||
|
startServer,
|
||||||
|
} from "./actions"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processFileType(file: string) {
|
export function processFileType(file: string) {
|
||||||
const ending = file.split(".").pop();
|
const ending = file.split(".").pop()
|
||||||
|
|
||||||
if (ending === "ts" || ending === "tsx") return "typescript";
|
if (ending === "ts" || ending === "tsx") return "typescript"
|
||||||
if (ending === "js" || ending === "jsx") return "javascript";
|
if (ending === "js" || ending === "jsx") return "javascript"
|
||||||
|
|
||||||
if (ending) return ending;
|
if (ending) return ending
|
||||||
return "plaintext";
|
return "plaintext"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateName(
|
export function validateName(
|
||||||
@ -25,7 +30,7 @@ export function validateName(
|
|||||||
type: "file" | "folder"
|
type: "file" | "folder"
|
||||||
) {
|
) {
|
||||||
if (newName === oldName || newName.length === 0) {
|
if (newName === oldName || newName.length === 0) {
|
||||||
return { status: false, message: "" };
|
return { status: false, message: "" }
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
newName.includes("/") ||
|
newName.includes("/") ||
|
||||||
@ -34,9 +39,9 @@ export function validateName(
|
|||||||
(type === "file" && !newName.includes(".")) ||
|
(type === "file" && !newName.includes(".")) ||
|
||||||
(type === "folder" && newName.includes("."))
|
(type === "folder" && newName.includes("."))
|
||||||
) {
|
) {
|
||||||
return { status: false, message: "Invalid file name." };
|
return { status: false, message: "Invalid file name." }
|
||||||
}
|
}
|
||||||
return { status: true, message: "" };
|
return { status: true, message: "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addNew(
|
export function addNew(
|
||||||
@ -49,9 +54,9 @@ export function addNew(
|
|||||||
setFiles((prev) => [
|
setFiles((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{ id: `projects/${sandboxData.id}/${name}`, name, type: "file" },
|
{ id: `projects/${sandboxData.id}/${name}`, name, type: "file" },
|
||||||
]);
|
])
|
||||||
} else {
|
} else {
|
||||||
console.log("adding folder");
|
console.log("adding folder")
|
||||||
setFiles((prev) => [
|
setFiles((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@ -60,48 +65,92 @@ export function addNew(
|
|||||||
type: "folder",
|
type: "folder",
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkServiceStatus(serviceName: string): Promise<Service> {
|
export function checkServiceStatus(serviceName: string): Promise<Service> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let tries = 0;
|
let tries = 0
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
tries++;
|
tries++
|
||||||
|
|
||||||
if (tries > 40) {
|
if (tries > 40) {
|
||||||
clearInterval(interval);
|
clearInterval(interval)
|
||||||
reject(new Error("Timed out."));
|
reject(new Error("Timed out."))
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await describeService(serviceName);
|
const response = await describeService(serviceName)
|
||||||
const activeServices = response.services?.filter(
|
const activeServices = response.services?.filter(
|
||||||
(service) => service.status === "ACTIVE"
|
(service) => service.status === "ACTIVE"
|
||||||
);
|
)
|
||||||
console.log("Checking activeServices status", activeServices);
|
console.log("Checking activeServices status", activeServices)
|
||||||
|
|
||||||
if (activeServices?.length === 1) {
|
if (activeServices?.length === 1) {
|
||||||
const service = activeServices?.[0];
|
const service = activeServices?.[0]
|
||||||
if (
|
if (
|
||||||
service.runningCount === service.desiredCount &&
|
service.runningCount === service.desiredCount &&
|
||||||
service.deployments?.length === 1
|
service.deployments?.length === 1
|
||||||
) {
|
) {
|
||||||
if (service.deployments[0].rolloutState === "COMPLETED") {
|
if (service.deployments[0].rolloutState === "COMPLETED") {
|
||||||
clearInterval(interval);
|
clearInterval(interval)
|
||||||
resolve(service);
|
resolve(service)
|
||||||
} else if (service.deployments[0].rolloutState === "FAILED") {
|
} else if (service.deployments[0].rolloutState === "FAILED") {
|
||||||
clearInterval(interval);
|
clearInterval(interval)
|
||||||
reject(new Error("Deployment failed."));
|
reject(new Error("Deployment failed."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
clearInterval(interval);
|
clearInterval(interval)
|
||||||
reject(error);
|
reject(error)
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000)
|
||||||
});
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupServer({
|
||||||
|
sandboxId,
|
||||||
|
setIsServiceRunning,
|
||||||
|
setIsDeploymentActive,
|
||||||
|
setTaskIp,
|
||||||
|
setDidFail,
|
||||||
|
toast,
|
||||||
|
}: {
|
||||||
|
sandboxId: string
|
||||||
|
setIsServiceRunning: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setIsDeploymentActive: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setTaskIp: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||||
|
setDidFail: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
toast: any
|
||||||
|
}) {
|
||||||
|
const doesExist = await doesServiceExist(sandboxId)
|
||||||
|
|
||||||
|
if (!doesExist) {
|
||||||
|
const response = await startServer(sandboxId)
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
toast.error(response.message)
|
||||||
|
setDidFail(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsServiceRunning(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!doesExist) {
|
||||||
|
await checkServiceStatus(sandboxId)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeploymentActive(true)
|
||||||
|
|
||||||
|
const taskIp = await getTaskIp(sandboxId)
|
||||||
|
setTaskIp(taskIp)
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("An error occurred while initializing your server.")
|
||||||
|
setDidFail(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user