Fix build logs overflow by using Alpine.js reactive bindings
Replaced manual DOM manipulation with a deploymentCard Alpine component that uses x-text bindings and x-ref for proper scrolling, matching the working container logs implementation.
This commit is contained in:
parent
aaa55fd153
commit
ee4afbde80
117
static/js/app.js
117
static/js/app.js
@ -295,6 +295,71 @@ document.addEventListener("alpine:init", () => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Deployment Card Component (for individual deployment cards)
|
||||||
|
// ============================================
|
||||||
|
Alpine.data("deploymentCard", (config) => ({
|
||||||
|
appId: config.appId,
|
||||||
|
deploymentId: config.deploymentId,
|
||||||
|
logs: "",
|
||||||
|
status: config.status || "",
|
||||||
|
pollInterval: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Read initial logs from script tag (avoids escaping issues)
|
||||||
|
const initialLogsEl = this.$el.querySelector(".initial-logs");
|
||||||
|
this.logs = initialLogsEl?.textContent || "Loading...";
|
||||||
|
|
||||||
|
// Only poll if deployment is in progress
|
||||||
|
if (Alpine.store("utils").isDeploying(this.status)) {
|
||||||
|
this.fetchLogs();
|
||||||
|
this.pollInterval = setInterval(() => this.fetchLogs(), 1000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.pollInterval) {
|
||||||
|
clearInterval(this.pollInterval);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchLogs() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/apps/${this.appId}/deployments/${this.deploymentId}/logs`,
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
this.logs = data.logs || "No logs available";
|
||||||
|
this.status = data.status;
|
||||||
|
|
||||||
|
// Scroll to bottom after update
|
||||||
|
this.$nextTick(() => {
|
||||||
|
Alpine.store("utils").scrollToBottom(this.$refs.logsWrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop polling if deployment is done
|
||||||
|
if (!Alpine.store("utils").isDeploying(data.status)) {
|
||||||
|
if (this.pollInterval) {
|
||||||
|
clearInterval(this.pollInterval);
|
||||||
|
this.pollInterval = null;
|
||||||
|
}
|
||||||
|
// Reload page to show final state with duration etc
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Logs fetch error:", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get statusBadgeClass() {
|
||||||
|
return Alpine.store("utils").statusBadgeClass(this.status);
|
||||||
|
},
|
||||||
|
|
||||||
|
get statusLabel() {
|
||||||
|
return Alpine.store("utils").statusLabel(this.status);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Deployments History Page Component
|
// Deployments History Page Component
|
||||||
// ============================================
|
// ============================================
|
||||||
@ -317,10 +382,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.fetchAppStatus();
|
this.fetchAppStatus();
|
||||||
setInterval(() => {
|
setInterval(() => this.fetchAppStatus(), 1000);
|
||||||
this.fetchAppStatus();
|
|
||||||
this.fetchDeploymentLogs();
|
|
||||||
}, 1000);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchAppStatus() {
|
async fetchAppStatus() {
|
||||||
@ -365,53 +427,6 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchDeploymentLogs() {
|
|
||||||
if (!this.currentDeploymentId || !this.isDeploying) return;
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`/apps/${this.appId}/deployments/${this.currentDeploymentId}/logs`,
|
|
||||||
);
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
// Update the deployment card
|
|
||||||
const card = document.querySelector(
|
|
||||||
`[data-deployment-id="${this.currentDeploymentId}"]`,
|
|
||||||
);
|
|
||||||
if (card) {
|
|
||||||
const logsContent = card.querySelector(".logs-content");
|
|
||||||
const logsWrapper = card.querySelector(".logs-wrapper");
|
|
||||||
const statusBadge = card.querySelector(".deployment-status");
|
|
||||||
|
|
||||||
if (logsContent) {
|
|
||||||
const newLogs = data.logs || "Loading...";
|
|
||||||
// Only update if content changed to avoid unnecessary reflows
|
|
||||||
if (logsContent.textContent !== newLogs) {
|
|
||||||
logsContent.textContent = newLogs;
|
|
||||||
// Scroll after content update - use double rAF to ensure DOM is ready
|
|
||||||
if (logsWrapper) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
logsWrapper.scrollTop = logsWrapper.scrollHeight;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusBadge) {
|
|
||||||
statusBadge.className =
|
|
||||||
"deployment-status " +
|
|
||||||
Alpine.store("utils").statusBadgeClass(data.status);
|
|
||||||
statusBadge.textContent = Alpine.store("utils").statusLabel(
|
|
||||||
data.status,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Logs fetch error:", err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
submitDeploy() {
|
submitDeploy() {
|
||||||
this.isDeploying = true;
|
this.isDeploying = true;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -27,7 +27,12 @@
|
|||||||
<div id="deployments-list" class="space-y-4">
|
<div id="deployments-list" class="space-y-4">
|
||||||
{{if .Deployments}}
|
{{if .Deployments}}
|
||||||
{{range .Deployments}}
|
{{range .Deployments}}
|
||||||
<div class="card p-6 deployment-card" data-deployment-id="{{.ID}}" data-status="{{.Status}}">
|
<div class="card p-6 deployment-card" data-deployment-id="{{.ID}}" data-status="{{.Status}}"
|
||||||
|
x-data="deploymentCard({
|
||||||
|
appId: '{{$.App.ID}}',
|
||||||
|
deploymentId: {{.ID}},
|
||||||
|
status: '{{.Status}}'
|
||||||
|
})">
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-4">
|
||||||
<div class="flex items-center gap-2 text-sm text-gray-500">
|
<div class="flex items-center gap-2 text-sm text-gray-500">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@ -40,7 +45,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="deployment-status {{if eq .Status "success"}}badge-success{{else if eq .Status "failed"}}badge-error{{else if eq .Status "building"}}badge-warning{{else if eq .Status "deploying"}}badge-info{{else}}badge-neutral{{end}}">{{if eq .Status "success"}}Success{{else if eq .Status "failed"}}Failed{{else if eq .Status "building"}}Building{{else if eq .Status "deploying"}}Deploying{{else}}{{.Status}}{{end}}</span>
|
<span class="deployment-status" x-bind:class="statusBadgeClass" x-text="statusLabel"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -70,7 +75,7 @@
|
|||||||
{{if or .Logs.Valid (eq .Status "building") (eq .Status "deploying")}}
|
{{if or .Logs.Valid (eq .Status "building") (eq .Status "deploying")}}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="flex items-start gap-4 mb-2">
|
<div class="flex items-start gap-4 mb-2">
|
||||||
<span class="cursor-pointer text-sm text-primary-600 hover:text-primary-800 font-medium">Build Logs</span>
|
<span class="text-sm text-primary-600 font-medium">Build Logs</span>
|
||||||
{{if or (eq .Status "success") (eq .Status "failed")}}
|
{{if or (eq .Status "success") (eq .Status "failed")}}
|
||||||
<a href="/apps/{{$.App.ID}}/deployments/{{.ID}}/download" class="text-sm text-primary-600 hover:text-primary-800 inline-flex items-center ml-auto" title="Download logs">
|
<a href="/apps/{{$.App.ID}}/deployments/{{.ID}}/download" class="text-sm text-primary-600 hover:text-primary-800 inline-flex items-center ml-auto" title="Download logs">
|
||||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@ -80,9 +85,10 @@
|
|||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="logs-wrapper bg-gray-900 rounded-lg p-4 overflow-y-scroll" style="max-height: 400px;">
|
<div x-ref="logsWrapper" class="bg-gray-900 rounded-lg p-4 overflow-auto" style="max-height: 400px;">
|
||||||
<pre class="logs-content text-gray-100 text-xs font-mono leading-relaxed whitespace-pre-wrap break-all">{{if .Logs.Valid}}{{.Logs.String}}{{else}}Loading...{{end}}</pre>
|
<pre class="text-gray-100 text-xs font-mono whitespace-pre-wrap" x-text="logs"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
{{if .Logs.Valid}}<script type="text/plain" class="initial-logs">{{.Logs.String}}</script>{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user