json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
225 lines (196 loc) • 7.49 kB
JavaScript
class FieldJobsContainer extends HTMLElement {
constructor() {
super();
this.objectId = null;
this.fieldName = null;
this.jobs = [];
this.updateInterval = null;
}
static get observedAttributes() {
return ['data-object-id', 'data-field-name'];
}
connectedCallback() {
this.objectId = this.getAttribute('data-object-id');
this.fieldName = this.getAttribute('data-field-name');
this.render();
this.startElapsedTimeUpdates();
// Immediately poll for jobs on connect
if (this.objectId && this.fieldName) {
var self = this;
$.get('/API/aijobs/' + encodeURIComponent(this.objectId) + '/' + encodeURIComponent(this.fieldName))
.then(function(data) {
if (data && data.jobs && Array.isArray(data.jobs)) {
self.updateJobs(data.jobs);
} else {
self.updateJobs([]);
}
})
.fail(function(err) {
// Silently fail - will retry on next poll
if (window.DEBUG_MODE || (window.$c && $c.DEBUG_MODE)) {
console.warn('[field-jobs-container] initial poll failed:', err);
}
});
}
}
disconnectedCallback() {
this.stopElapsedTimeUpdates();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'data-object-id') {
this.objectId = newValue;
} else if (name === 'data-field-name') {
this.fieldName = newValue;
}
if (this.objectId && this.fieldName) {
this.render();
}
}
/**
* Update jobs from server response
*/
updateJobs(jobs) {
if (!Array.isArray(jobs)) {
jobs = [];
}
this.jobs = jobs;
this.render();
}
/**
* Calculate elapsed seconds from startTime
*/
calculateElapsed(startTime) {
if (!startTime) return 0;
try {
var start = new Date(startTime);
var now = new Date();
var elapsedMs = now - start;
return Math.floor(elapsedMs / 1000);
} catch (e) {
return 0;
}
}
/**
* Start elapsed time updates (every second)
*/
startElapsedTimeUpdates() {
if (this.updateInterval) return; // Already running
this.updateInterval = setInterval(() => {
// Check if we have any active jobs
var hasActive = this.jobs.some(function(job) {
return (job.status === 'running' || job.status === 'starting') &&
(job.total == null || job.progress == null || job.progress < job.total);
});
if (hasActive) {
// Force re-render to update elapsed times
this.render();
} else {
// Stop if no active jobs
this.stopElapsedTimeUpdates();
}
}, 1000);
}
/**
* Stop elapsed time updates
*/
stopElapsedTimeUpdates() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
}
/**
* Render the component
*/
render() {
// Build endpoint URL for token link (if we have both objectId and fieldName)
var endpointUrl = null;
var tokenLinkHtml = '';
var fullToken = null;
// Try to get a sample token from jobs, or construct lookup key
if (this.jobs.length > 0 && this.jobs[0].token) {
fullToken = this.jobs[0].token;
} else if (this.objectId && this.fieldName) {
// If no jobs, show lookup key format
fullToken = this.objectId + '_' + this.fieldName;
}
if (this.objectId && this.fieldName) {
endpointUrl = '/API/aijobs/' + encodeURIComponent(this.objectId) + '/' + encodeURIComponent(this.fieldName);
var titleAttr = fullToken ? 'title="' + fullToken.replace(/"/g, '"') + '"' : 'title="View endpoint"';
tokenLinkHtml = '<a href="' + endpointUrl + '" target="_blank" class="field-jobs-token-link" ' + titleAttr + '>[' + this.objectId.substring(0, 8) + '...|' + this.fieldName + ']</a>';
}
if (!this.objectId || !this.fieldName) {
var html = '<div class="field-jobs-title">';
html += '<span class="field-jobs-title-text">0 active jobs</span>';
html += tokenLinkHtml;
html += '</div>';
this.innerHTML = html;
return;
}
// Filter active jobs (for count)
var activeJobs = this.jobs.filter(function(job) {
return job.status !== 'complete' && job.status !== 'error';
});
// Update token link with full token if we have jobs (recalculate in case jobs were added)
if (this.jobs.length > 0 && this.jobs[0].token) {
fullToken = this.jobs[0].token;
if (endpointUrl) {
var titleAttr = 'title="' + fullToken.replace(/"/g, '"') + '"';
tokenLinkHtml = '<a href="' + endpointUrl + '" target="_blank" class="field-jobs-token-link" ' + titleAttr + '>[' + this.objectId.substring(0, 8) + '...|' + this.fieldName + ']</a>';
}
}
// Build HTML
var html = '';
// Title with active job count and token link on the right
html += '<div class="field-jobs-title">';
html += '<span class="field-jobs-title-text">' + activeJobs.length + ' active job' + (activeJobs.length !== 1 ? 's' : '') + '</span>';
html += tokenLinkHtml;
html += '</div>';
// Job rows (show all jobs, including completed)
if (this.jobs.length > 0) {
var self = this;
this.jobs.forEach(function(job) {
var jobName = job.promptName || job.promptId || 'Job';
var elapsedSeconds = job.elapsedSeconds != null ? job.elapsedSeconds : self.calculateElapsed(job.startTime);
var elapsedText = elapsedSeconds > 0 ? ' (' + elapsedSeconds + 's)' : '';
// Status text (capitalized)
var statusText = '';
if (job.status) {
statusText = ' - ' + job.status.charAt(0).toUpperCase() + job.status.slice(1);
}
var percent = null;
if (job.total != null && job.progress != null) {
percent = Math.round((job.progress / job.total) * 100);
}
var statusClass = '';
if (job.status === 'complete') {
statusClass = 'field-jobs-complete';
percent = 100;
} else if (job.status === 'error') {
statusClass = 'field-jobs-error';
}
html += '<div class="field-jobs-row ' + statusClass + '">';
html += '<div class="field-jobs-row-content">';
html += '<div class="field-jobs-row-title">' + jobName + statusText + elapsedText + '</div>';
html += '<div class="field-jobs-row-message">' + (job.message || '') + '</div>';
html += '</div>';
if (percent != null) {
html += '<div class="field-jobs-row-percent">' + percent + '%</div>';
} else {
html += '<div class="field-jobs-row-percent">—</div>';
}
html += '</div>';
});
}
this.innerHTML = html;
// Restart elapsed time updates if we have active jobs
var hasActive = this.jobs.some(function(job) {
return (job.status === 'running' || job.status === 'starting') &&
(job.total == null || job.progress == null || job.progress < job.total);
});
if (hasActive) {
this.startElapsedTimeUpdates();
}
}
}
window.customElements.define('field-jobs-container', FieldJobsContainer);