Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: first attempt of terminal #285

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@sveltejs/adapter-node": "^5.0.1",
"@sveltejs/adapter-static": "^3.0.1",
"@webcontainer/api": "^1.1.9",
"@xterm/xterm": "^5.5.0",
"@xyflow/svelte": "^0.1.3",
"chart.js": "^4.4.3",
"diff": "^5.2.0",
Expand All @@ -67,6 +68,8 @@
"svelte-french-toast": "^1.2.0",
"svelte-local-storage-store": "^0.6.4",
"svelte-monaco": "^0.3.0",
"svelte-purify": "^1.1.27"
"svelte-purify": "^1.1.27",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
}
}
157 changes: 157 additions & 0 deletions frontend/src/lib/components/map/Console.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import darkTheme from "$lib/stores/theme";
import { terminal_size } from "$lib/stores/terminal";

import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";

export let podName: string;
export let projectId: string;

const terminal = new Terminal({
convertEol: true,
disableStdin: false,
cursorBlink: true,
fontFamily: "monospace",
fontSize: 14,
theme: $darkTheme
? {
foreground: "#d2d2d2",
background: "#2B3441",
cursor: "#adadad",
black: "#000000",
red: "#d81e00",
green: "#5ea702",
yellow: "#cfae00",
blue: "#427ab3",
magenta: "#89658e",
cyan: "#00a7aa",
white: "#dbded8",
brightBlack: "#686a66",
brightRed: "#f54235",
brightGreen: "#99e343",
brightYellow: "#fdeb61",
brightBlue: "#84b0d8",
brightMagenta: "#bc94b7",
brightCyan: "#37e6e8",
brightWhite: "#f1f1f0"
}
: {
foreground: "#d2d2d2",
background: "#2B3441",
cursor: "#adadad",
black: "#000000",
red: "#d81e00",
green: "#5ea702",
yellow: "#cfae00",
blue: "#427ab3",
magenta: "#89658e",
cyan: "#00a7aa",
white: "#dbded8",
brightBlack: "#686a66",
brightRed: "#f54235",
brightGreen: "#99e343",
brightYellow: "#fdeb61",
brightBlue: "#84b0d8",
brightMagenta: "#bc94b7",
brightCyan: "#37e6e8",
brightWhite: "#f1f1f0"
},
scrollOnUserInput: true
});

let socket: WebSocket;
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);

const connectWebSocket = () => {
let host = window.location.host;
if (host.includes("localhost")) {
host = "localhost:8090";
}

const protocol = window.location.protocol === "https:" ? "wss" : "ws";
socket = new WebSocket(`${protocol}://${host}/ws/k8s/terminal`);

socket.binaryType = "arraybuffer";

socket.onopen = () => {
const message = JSON.stringify({ projectId, podName });
socket.send(message);
terminal.onData((data) => {
socket.send(new TextEncoder().encode(data));
});
};

socket.onmessage = (event: MessageEvent) => {
if (event.data instanceof ArrayBuffer) {
terminal.write(new TextDecoder().decode(event.data));
}
};

socket.onerror = (error: Event) => {
const errorMessage = (error as ErrorEvent).message || "An error occurred";
terminal.write(`\r\nWebSocket error: ${errorMessage}\r\n`);
};

socket.onclose = () => {
terminal.write("\r\nWebSocket closed. Attempting to reconnect...\r\n");
setTimeout(connectWebSocket, 5000);
};
};

terminal.onResize((size) => {
const terminal_size = {
cols: size.cols,
rows: size.rows,
y: size.rows,
x: size.cols
};
if (socket.readyState === WebSocket.OPEN) {
socket.send(new TextEncoder().encode("\x01" + JSON.stringify(terminal_size)));
}
});

let div: HTMLDivElement;

onMount(() => {
connectWebSocket();
terminal.open(div);
setTimeout(() => {
fitAddon.fit();
}, 300);
});

onDestroy(() => {
socket?.close();
terminal.dispose();
});

export const update_height = () => {
fitAddon.fit();
};

$: {
$terminal_size;
setTimeout(() => {
update_height();
}, 300);
}
</script>

<div bind:this={div} style="height: 100%; width: 100%;" />

<style>
div {
height: 100%;
width: 100%;
}
div :global(.xterm) {
height: 100%;
padding: 5px;
}
div :global(.xterm-viewport) {
overflow-y: hidden !important;
}
</style>
14 changes: 14 additions & 0 deletions frontend/src/lib/components/map/Desktop.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import darkTheme from "$lib/stores/theme";
import type { ComponentType, SvelteComponent } from "svelte";
// eslint-disable-next-line @typescript-eslint/naming-convention
export let Console: ComponentType<SvelteComponent>;
export let podName: string;
export let projectId: string;
</script>

{#key $darkTheme}
<div class="h-full overflow-y-auto">
<svelte:component this={Console} {podName} {projectId} />
</div>
{/key}
9 changes: 9 additions & 0 deletions frontend/src/lib/components/map/PlaceholderComponent.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div />

<style>
div {
background-color: var(--sk-back-1);
width: 100%;
height: 100%;
}
</style>
26 changes: 26 additions & 0 deletions frontend/src/lib/components/map/ShellObject.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
import { onDestroy, onMount, type ComponentType, type SvelteComponent } from "svelte";
import PlaceholderComponent from "./PlaceholderComponent.svelte";
import Desktop from "./Desktop.svelte";
export let podName: string;
export let projectId: string;

let Console: ComponentType<SvelteComponent> = PlaceholderComponent;
let clear: number;
// let loadingCodeEditor = false;

onMount(async () => {
// loadingCodeEditor = true;
Console = (await import("$lib/components/map/Console.svelte")).default;
});

onDestroy(() => {
// loadingCodeEditor = false;
Console = PlaceholderComponent;
clearInterval(clear);
});
</script>

<div class="p-2 rounded-lg bg-gray-800">
<Desktop {Console} {podName} {projectId} />
</div>
6 changes: 6 additions & 0 deletions frontend/src/lib/stores/terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { writable } from "svelte/store";

export const terminal_size = writable({ height: 65 });

// get pathnames from url
export const pathname = writable(window.location.pathname);
5 changes: 3 additions & 2 deletions frontend/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import "../app.postcss";
import "../app.css";
import "../styles/app.postcss";
import "../styles/app.css";
import "../styles/xterm.css";
import { metadata } from "$lib/stores/metadata";
import { site } from "$lib/config";
import { beforeNavigate } from "$app/navigation";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
Lock,
NetworkIcon,
ScrollText,
TerminalSquare,
Trash,
X
} from "lucide-svelte";
Expand All @@ -29,6 +30,8 @@
import EventStream from "$lib/components/map/EventStream.svelte";
import toast from "svelte-french-toast";
import selectedDeploymentId from "$lib/stores/deployment";
import ShellObject from "$lib/components/map/ShellObject.svelte";
import selectedProjectId from "$lib/stores/project";

let transitionParamsRight = {
x: 320,
Expand Down Expand Up @@ -599,6 +602,13 @@
</div>
<LogStream podName={$selectedNode?.name ?? ""} />
</TabItem>
<TabItem>
<div slot="title" class="flex items-center gap-2">
<TerminalSquare />
Shell
</div>
<ShellObject podName={$selectedNode?.name ?? ""} projectId={$selectedProjectId} />
</TabItem>
<TabItem>
<div slot="title" class="flex items-center gap-2">
<Bell />
Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading