Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0117e197bb | ||
|
37ebe41526 | ||
|
ed367ed27c | ||
|
9666401625 | ||
|
22d638a090 | ||
|
0dd99cbc77 |
@ -174,6 +174,15 @@ Setting up deployments first requires a separate domain (such as gitwit.app, whi
|
||||
|
||||
We then deploy Dokku on a separate server, according to this guide: https://dev.to/jamesmurdza/host-your-own-paas-platform-as-a-service-on-amazon-web-services-3f0d
|
||||
|
||||
And we install [dokku-daemon](https://github.com/dokku/dokku-daemon) with the following commands:
|
||||
|
||||
```
|
||||
git clone https://github.com/dokku/dokku-daemon
|
||||
cd dokku-daemon
|
||||
sudo make install
|
||||
systemctl start dokku-daemon
|
||||
```
|
||||
|
||||
The Sandbox platform connects to the Dokku server via SSH, using SSH keys specifically generated for this connection. The SSH key is stored on the Sandbox server, and the following environment variables are set in /backend/server/.env:
|
||||
|
||||
```bash
|
||||
|
@ -326,25 +326,69 @@ export class FileManager {
|
||||
// Save file content
|
||||
async saveFile(fileId: string, body: string): Promise<void> {
|
||||
if (!fileId) return // handles saving when no file is open
|
||||
|
||||
|
||||
if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) {
|
||||
throw new Error("File size too large. Please reduce the file size.")
|
||||
}
|
||||
|
||||
// Save to remote storage
|
||||
await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body)
|
||||
|
||||
|
||||
// Update local file data cache
|
||||
let file = this.fileData.find((f) => f.id === fileId)
|
||||
if (file) {
|
||||
file.data = body
|
||||
} else {
|
||||
// If the file wasn't in our cache, add it
|
||||
file = {
|
||||
id: fileId,
|
||||
data: body,
|
||||
}
|
||||
this.fileData.push(file)
|
||||
}
|
||||
|
||||
await this.sandbox.files.write(path.posix.join(this.dirName, fileId), body)
|
||||
|
||||
// Save to sandbox filesystem
|
||||
const filePath = path.posix.join(this.dirName, fileId)
|
||||
await this.sandbox.files.write(filePath, body)
|
||||
|
||||
// Instead of updating the entire file structure, just ensure this file exists in it
|
||||
const parts = fileId.split('/').filter(Boolean)
|
||||
let current = this.files
|
||||
let currentPath = ''
|
||||
|
||||
// Navigate/create the path to the file
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentPath += '/' + parts[i]
|
||||
let folder = current.find(
|
||||
(f) => f.type === 'folder' && f.name === parts[i]
|
||||
) as TFolder
|
||||
|
||||
if (!folder) {
|
||||
folder = {
|
||||
id: currentPath,
|
||||
type: 'folder',
|
||||
name: parts[i],
|
||||
children: [],
|
||||
}
|
||||
current.push(folder)
|
||||
}
|
||||
current = folder.children
|
||||
}
|
||||
|
||||
// Add/update the file in the structure if it doesn't exist
|
||||
const fileName = parts[parts.length - 1]
|
||||
const existingFile = current.find(
|
||||
(f) => f.type === 'file' && f.name === fileName
|
||||
)
|
||||
|
||||
if (!existingFile) {
|
||||
current.push({
|
||||
id: fileId,
|
||||
type: 'file',
|
||||
name: fileName,
|
||||
})
|
||||
}
|
||||
|
||||
this.refreshFileList?.(this.files)
|
||||
this.fixPermissions()
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,10 @@ export class SSHSocketClient {
|
||||
private conn: Client
|
||||
private config: SSHConfig
|
||||
private socketPath: string
|
||||
private isConnected: boolean = false
|
||||
private _isConnected: boolean = false
|
||||
public get isConnected(): boolean {
|
||||
return this._isConnected
|
||||
}
|
||||
|
||||
// Constructor initializes the SSH client and sets up configuration
|
||||
constructor(config: SSHConfig, socketPath: string) {
|
||||
@ -34,7 +37,7 @@ export class SSHSocketClient {
|
||||
private closeConnection() {
|
||||
console.log("Closing SSH connection...")
|
||||
this.conn.end()
|
||||
this.isConnected = false
|
||||
this._isConnected = false
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
@ -44,17 +47,17 @@ export class SSHSocketClient {
|
||||
this.conn
|
||||
.on("ready", () => {
|
||||
console.log("SSH connection established")
|
||||
this.isConnected = true
|
||||
this._isConnected = true
|
||||
resolve()
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error("SSH connection error:", err)
|
||||
this.isConnected = false
|
||||
this._isConnected = false
|
||||
reject(err)
|
||||
})
|
||||
.on("close", () => {
|
||||
console.log("SSH connection closed")
|
||||
this.isConnected = false
|
||||
this._isConnected = false
|
||||
})
|
||||
.connect(this.config)
|
||||
})
|
||||
@ -86,10 +89,13 @@ export class SSHSocketClient {
|
||||
)
|
||||
})
|
||||
.on("data", (data: Buffer) => {
|
||||
// Netcat remains open until it is closed, so we close the connection once we receive data.
|
||||
resolve(data.toString())
|
||||
stream.close()
|
||||
})
|
||||
.stderr.on("data", (data: Buffer) => {
|
||||
reject(new Error(data.toString()))
|
||||
stream.close()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -170,8 +170,20 @@ export class Sandbox {
|
||||
|
||||
// Handle checking if an app exists
|
||||
const handleAppExists: SocketHandler = async ({ appName }) => {
|
||||
if (!this.dokkuClient)
|
||||
throw new Error("Failed to check app existence: No Dokku client")
|
||||
if (!this.dokkuClient) {
|
||||
console.log("Failed to check app existence: No Dokku client")
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
if (!this.dokkuClient.isConnected) {
|
||||
console.log(
|
||||
"Failed to check app existence: The Dokku client is not connected"
|
||||
)
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
exists: await this.dokkuClient.appExists(appName),
|
||||
|
@ -387,9 +387,16 @@ export default function CodeEditor({
|
||||
(mergedCode: string, originalCode: string) => {
|
||||
if (!editorRef) return
|
||||
|
||||
const model = editorRef.getModel()
|
||||
if (!model) return // Store original content
|
||||
;(model as any).originalContent = originalCode
|
||||
|
||||
// Calculate the full range of the document
|
||||
const fullRange = model.getFullModelRange()
|
||||
|
||||
// Create decorations before applying the edit
|
||||
const originalLines = originalCode.split("\n")
|
||||
const mergedLines = mergedCode.split("\n")
|
||||
|
||||
const decorations: monaco.editor.IModelDeltaDecoration[] = []
|
||||
|
||||
for (
|
||||
@ -410,14 +417,16 @@ export default function CodeEditor({
|
||||
}
|
||||
}
|
||||
|
||||
// Store original content in the model
|
||||
const model = editorRef.getModel()
|
||||
if (model) {
|
||||
;(model as any).originalContent = originalCode
|
||||
}
|
||||
editorRef.setValue(mergedCode)
|
||||
// Execute the edit operation
|
||||
editorRef.executeEdits("apply-code", [
|
||||
{
|
||||
range: fullRange,
|
||||
text: mergedCode,
|
||||
forceMoveMarkers: true,
|
||||
},
|
||||
])
|
||||
|
||||
// Apply decorations
|
||||
// Apply decorations after the edit
|
||||
const newDecorations = editorRef.createDecorationsCollection(decorations)
|
||||
setMergeDecorationsCollection(newDecorations)
|
||||
},
|
||||
@ -784,31 +793,32 @@ export default function CodeEditor({
|
||||
setGenerate((prev) => ({ ...prev, show: false }))
|
||||
|
||||
// Check if the tab already exists in the list of open tabs
|
||||
const exists = tabs.find((t) => t.id === tab.id)
|
||||
setTabs((prev) => {
|
||||
if (exists) {
|
||||
// If the tab exists, make it the active tab
|
||||
setActiveFileId(exists.id)
|
||||
return prev
|
||||
}
|
||||
// If the tab doesn't exist, add it to the list of tabs and make it active
|
||||
return [...prev, tab]
|
||||
})
|
||||
const existingTab = tabs.find((t) => t.id === tab.id)
|
||||
|
||||
// If the file's content is already cached, set it as the active content
|
||||
if (fileContents[tab.id]) {
|
||||
setActiveFileContent(fileContents[tab.id])
|
||||
if (existingTab) {
|
||||
// If the tab exists, just make it active
|
||||
setActiveFileId(existingTab.id)
|
||||
if (fileContents[existingTab.id]) {
|
||||
setActiveFileContent(fileContents[existingTab.id])
|
||||
}
|
||||
} else {
|
||||
// Otherwise, fetch the content of the file and cache it
|
||||
debouncedGetFile(tab.id, (response: string) => {
|
||||
setFileContents((prev) => ({ ...prev, [tab.id]: response }))
|
||||
setActiveFileContent(response)
|
||||
})
|
||||
// If the tab doesn't exist, add it to the list and make it active
|
||||
setTabs((prev) => [...prev, tab])
|
||||
|
||||
// Fetch content if not cached
|
||||
if (!fileContents[tab.id]) {
|
||||
debouncedGetFile(tab.id, (response: string) => {
|
||||
setFileContents((prev) => ({ ...prev, [tab.id]: response }))
|
||||
setActiveFileContent(response)
|
||||
})
|
||||
} else {
|
||||
setActiveFileContent(fileContents[tab.id])
|
||||
}
|
||||
}
|
||||
|
||||
// Set the editor language based on the file type
|
||||
setEditorLanguage(processFileType(tab.name))
|
||||
// Set the active file ID to the new tab
|
||||
// Set the active file ID
|
||||
setActiveFileId(tab.id)
|
||||
}
|
||||
|
||||
|
@ -21,12 +21,14 @@ export default function DeployButtonModal({
|
||||
const { deploy, getAppExists } = useTerminal()
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [isDeployed, setIsDeployed] = useState(false)
|
||||
const [deployButtonVisible, setDeployButtonEnabled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkDeployment = async () => {
|
||||
if (getAppExists) {
|
||||
const exists = await getAppExists(data.id)
|
||||
setIsDeployed(exists)
|
||||
setDeployButtonEnabled(exists.success)
|
||||
setIsDeployed((exists.success && exists.exists) ?? false)
|
||||
}
|
||||
}
|
||||
checkDeployment()
|
||||
@ -50,10 +52,12 @@ export default function DeployButtonModal({
|
||||
<>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
Deploy
|
||||
</Button>
|
||||
{deployButtonVisible && (
|
||||
<Button variant="outline">
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
Deploy
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg"
|
||||
|
@ -20,7 +20,9 @@ interface TerminalContextType {
|
||||
createNewTerminal: (command?: string) => Promise<void>
|
||||
closeTerminal: (id: string) => void
|
||||
deploy: (callback: () => void) => void
|
||||
getAppExists: ((appName: string) => Promise<boolean>) | null
|
||||
getAppExists:
|
||||
| ((appName: string) => Promise<{ success: boolean; exists?: boolean }>)
|
||||
| null
|
||||
}
|
||||
|
||||
const TerminalContext = createContext<TerminalContextType | undefined>(
|
||||
@ -92,16 +94,18 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const getAppExists = async (appName: string): Promise<boolean> => {
|
||||
const getAppExists = async (
|
||||
appName: string
|
||||
): Promise<{ success: boolean; exists?: boolean }> => {
|
||||
console.log("Is there a socket: " + !!socket)
|
||||
if (!socket) {
|
||||
console.error("Couldn't check if app exists: No socket")
|
||||
return false
|
||||
return { success: false }
|
||||
}
|
||||
const response: { exists: boolean } = await new Promise((resolve) =>
|
||||
socket.emit("getAppExists", { appName }, resolve)
|
||||
const response: { success: boolean; exists?: boolean } = await new Promise(
|
||||
(resolve) => socket.emit("getAppExists", { appName }, resolve)
|
||||
)
|
||||
return response.exists
|
||||
return response
|
||||
}
|
||||
|
||||
const value = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user