All checks were successful
Check / check (pull_request) Successful in 11m29s
Split static/js/app.js (581 lines) into 5 focused modules: - utils.js: global utilities store + legacy compat - components.js: reusable Alpine.js components (copy, confirm, dismiss, time) - app-detail.js: app detail page logic (status polling, logs) - deployment.js: deployment card + deployments history page - dashboard.js: dashboard relative time updates Closes #128
177 lines
5.3 KiB
JavaScript
177 lines
5.3 KiB
JavaScript
/**
|
|
* upaas - Deployment Components
|
|
*
|
|
* Deployment card (individual deployment log viewer) and
|
|
* deployments history page (list of all deployments).
|
|
*/
|
|
|
|
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,
|
|
_autoScroll: true,
|
|
|
|
init() {
|
|
// Read initial logs from script tag (avoids escaping issues)
|
|
const initialLogsEl = this.$el.querySelector(".initial-logs");
|
|
this.logs = initialLogsEl?.dataset.logs || "Loading...";
|
|
|
|
// Set up scroll tracking
|
|
this.$nextTick(() => {
|
|
const wrapper = this.$refs.logsWrapper;
|
|
if (wrapper) {
|
|
wrapper.addEventListener('scroll', () => {
|
|
this._autoScroll = Alpine.store("utils").isScrolledToBottom(wrapper);
|
|
}, { passive: true });
|
|
}
|
|
});
|
|
|
|
// 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();
|
|
const newLogs = data.logs || "No logs available";
|
|
const logsChanged = newLogs !== this.logs;
|
|
this.logs = newLogs;
|
|
this.status = data.status;
|
|
|
|
// Scroll to bottom only when content changes AND user hasn't scrolled up
|
|
if (logsChanged && this._autoScroll) {
|
|
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
|
|
// ============================================
|
|
Alpine.data("deploymentsPage", (config) => ({
|
|
appId: config.appId,
|
|
currentDeploymentId: null,
|
|
isDeploying: false,
|
|
|
|
init() {
|
|
// Check for in-progress deployments on page load
|
|
const inProgressCard = document.querySelector(
|
|
'[data-status="building"], [data-status="deploying"]',
|
|
);
|
|
if (inProgressCard) {
|
|
this.currentDeploymentId = parseInt(
|
|
inProgressCard.getAttribute("data-deployment-id"),
|
|
10,
|
|
);
|
|
this.isDeploying = true;
|
|
}
|
|
|
|
this.fetchAppStatus();
|
|
this._scheduleStatusPoll();
|
|
},
|
|
|
|
_statusPollTimer: null,
|
|
|
|
_scheduleStatusPoll() {
|
|
if (this._statusPollTimer) clearTimeout(this._statusPollTimer);
|
|
const interval = this.isDeploying ? 1000 : 10000;
|
|
this._statusPollTimer = setTimeout(() => {
|
|
this.fetchAppStatus();
|
|
this._scheduleStatusPoll();
|
|
}, interval);
|
|
},
|
|
|
|
async fetchAppStatus() {
|
|
try {
|
|
const res = await fetch(`/apps/${this.appId}/status`);
|
|
const data = await res.json();
|
|
// Use deployment status, not app status - it's more reliable during transitions
|
|
const deploying = Alpine.store("utils").isDeploying(
|
|
data.latestDeploymentStatus,
|
|
);
|
|
|
|
// Detect new deployment
|
|
if (
|
|
data.latestDeploymentID &&
|
|
data.latestDeploymentID !== this.currentDeploymentId
|
|
) {
|
|
// Check if we have a card for this deployment
|
|
const hasCard = document.querySelector(
|
|
`[data-deployment-id="${data.latestDeploymentID}"]`,
|
|
);
|
|
|
|
if (deploying && !hasCard) {
|
|
// New deployment started but no card exists - reload to show it
|
|
window.location.reload();
|
|
|
|
return;
|
|
}
|
|
|
|
this.currentDeploymentId = data.latestDeploymentID;
|
|
}
|
|
|
|
// Update deploying state based on latest deployment status
|
|
if (deploying && !this.isDeploying) {
|
|
this.isDeploying = true;
|
|
this._scheduleStatusPoll(); // Switch to fast polling
|
|
} else if (!deploying && this.isDeploying) {
|
|
// Deployment finished - reload to show final state
|
|
this.isDeploying = false;
|
|
window.location.reload();
|
|
}
|
|
} catch (err) {
|
|
console.error("Status fetch error:", err);
|
|
}
|
|
},
|
|
|
|
submitDeploy() {
|
|
this.isDeploying = true;
|
|
},
|
|
|
|
formatTime(isoTime) {
|
|
return Alpine.store("utils").formatRelativeTime(isoTime);
|
|
},
|
|
}));
|
|
});
|