Project Page Frontend Update
This commit is contained in:
155
frontend/src/lib/project-dashboard.svelte.ts
Normal file
155
frontend/src/lib/project-dashboard.svelte.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { getProject, redeployProject, stopProject, type Project, type Deployment } from "$lib/api";
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
export class ProjectState {
|
||||
project = $state<Project | null>(null);
|
||||
loading = $state(true);
|
||||
activeDeploymentId = $state<string | null>(null);
|
||||
|
||||
// We keep a string copy of logs for clipboard and initial loading,
|
||||
// though xterm manages its own buffer.
|
||||
activeDeploymentLogs = $state("");
|
||||
|
||||
ws: WebSocket | null = null;
|
||||
onLogData: ((data: string) => void) | null = null;
|
||||
onLogClear: (() => void) | null = null;
|
||||
|
||||
latestDeployment = $derived(this.project?.deployments?.[0]);
|
||||
status = $derived(this.latestDeployment?.status || "unknown");
|
||||
|
||||
constructor(public projectId: string) {}
|
||||
|
||||
async init() {
|
||||
await this.loadProject();
|
||||
this.startStatusPoll();
|
||||
}
|
||||
|
||||
private pollInterval: number | null = null;
|
||||
|
||||
startStatusPoll() {
|
||||
if (typeof window === "undefined") return;
|
||||
if (this.pollInterval) window.clearInterval(this.pollInterval);
|
||||
this.pollInterval = window.setInterval(() => {
|
||||
if (this.status === "building") {
|
||||
this.loadProject();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async loadProject() {
|
||||
if (!this.projectId) return;
|
||||
const res = await getProject(this.projectId);
|
||||
if (res) {
|
||||
this.project = res;
|
||||
const active = res.deployments?.find((d) => d.id === this.activeDeploymentId) ?? res.deployments?.[0];
|
||||
if (active) {
|
||||
this.selectDeployment(active, true);
|
||||
}
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.loading = true;
|
||||
await this.loadProject();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async handleRedeploy() {
|
||||
if (!this.project) return;
|
||||
toast.info("Starting redeployment...");
|
||||
const success = await redeployProject(this.project.id);
|
||||
if (success) {
|
||||
toast.success("Redeployment started!");
|
||||
setTimeout(() => this.loadProject(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async handleStop() {
|
||||
if (!this.project) return;
|
||||
toast.info("Stopping project...");
|
||||
const success = await stopProject(this.project.id);
|
||||
if (success) {
|
||||
setTimeout(() => this.loadProject(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
selectDeployment(deployment: Deployment, force = false) {
|
||||
if (this.activeDeploymentId === deployment.id && !force) return;
|
||||
|
||||
this.activeDeploymentId = deployment.id;
|
||||
this.activeDeploymentLogs = deployment.logs || "";
|
||||
|
||||
// Reset xterm
|
||||
if (this.onLogClear) this.onLogClear();
|
||||
|
||||
// Write existing logs
|
||||
// We format CR LF for xterm
|
||||
const formattedLogs = (deployment.logs || "").replace(/\n/g, "\r\n");
|
||||
if (this.onLogData) this.onLogData(formattedLogs);
|
||||
|
||||
if (deployment.status === "building") {
|
||||
this.connectWebSocket(deployment.id);
|
||||
} else {
|
||||
this.closeWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
handleBuildCompleted() {
|
||||
// When a build finishes, refresh project data to update status/UI.
|
||||
this.loadProject();
|
||||
}
|
||||
|
||||
connectWebSocket(deploymentId: string) {
|
||||
this.closeWebSocket();
|
||||
// Ensure we are in browser
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
this.ws = new WebSocket(
|
||||
`${protocol}//${window.location.hostname}:8080/api/deployments/${deploymentId}/logs/stream`,
|
||||
);
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.activeDeploymentLogs += event.data;
|
||||
// Pass raw data to xterm, it handles ANSI codes.
|
||||
// Ensure newlines are treated as CRLF for terminal
|
||||
const chunk = event.data.replace(/\n/g, "\r\n");
|
||||
if (this.onLogData) {
|
||||
this.onLogData(chunk);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.log("Log stream closed");
|
||||
// If we were building, pull latest status once stream ends.
|
||||
if (this.status === "building") {
|
||||
this.loadProject();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
closeWebSocket() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
copyLogs() {
|
||||
if (typeof navigator !== "undefined") {
|
||||
navigator.clipboard.writeText(this.activeDeploymentLogs);
|
||||
toast.success("Logs copied to clipboard");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.closeWebSocket();
|
||||
if (this.pollInterval) {
|
||||
window.clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user