Files

781 lines
32 KiB
JavaScript

import { BUTTON_STYLES, UI_STYLES, STATUS_COLORS, UI_COLORS, TIMEOUTS } from './constants.js';
import { createButtonHelper as createButtonHelperFn } from './ui/buttonHelpers.js';
import { showCloudflareWarning as showCloudflareWarningFn } from './ui/cloudflareWarning.js';
import { createWorkerSettingsForm as createWorkerSettingsFormFn } from './ui/settingsForm.js';
import { renderEntityCard as renderEntityCardFn } from './ui/entityCard.js';
import { createLogModal } from './ui/logModal.js';
import { launchWorker, stopWorker, updateWorkerControls, viewWorkerLog } from './workerLifecycle.js';
import { isRemoteWorker } from './workerSettings.js';
const cardConfigs = {
master: {
checkbox: {
enabled: true,
masterToggle: true,
title: "Toggle master participation in workloads"
},
statusDot: {
id: 'master-status',
initialColor: (_, extension) => extension.isMasterParticipating() ? STATUS_COLORS.ONLINE_GREEN : STATUS_COLORS.DISABLED_GRAY,
initialTitle: (_, extension) => extension.isMasterParticipating() ? 'Master participating' : 'Master orchestrator only',
dynamic: true
},
infoText: (data, extension) => {
const cudaDevice = extension.config?.master?.cuda_device ?? extension.masterCudaDevice;
const cudaInfo = cudaDevice !== undefined ? `CUDA ${cudaDevice} • ` : '';
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
const participationEnabled = extension.isMasterParticipationEnabled();
const fallbackActive = extension.isMasterFallbackActive();
let delegateBadge = '';
if (!participationEnabled && fallbackActive) {
delegateBadge = `<br><span class="dist-worker-info__fallback">Fallback active • Master executing</span>`;
}
return `<span class="dist-worker-info__title" id="master-name-display">${data?.name || extension.config?.master?.name || "Master"}</span><br><span class="dist-worker-info__meta"><span id="master-cuda-info">${cudaInfo}Port ${port}</span></span>${delegateBadge}`;
},
controls: {
type: 'master'
},
settings: {
formType: 'master',
id: 'master-settings',
expandedTracker: 'masterSettingsExpanded'
},
hover: true,
expand: true,
border: 'solid'
},
worker: {
checkbox: {
enabled: true,
title: "Enable/disable this worker"
},
statusDot: {
dynamic: true,
initialColor: (data) => data.enabled ? STATUS_COLORS.OFFLINE_RED : STATUS_COLORS.DISABLED_GRAY,
initialTitle: (data) => data.enabled ? "Checking status..." : "Disabled",
id: (data) => `status-${data.id}`
},
infoText: (data, extension) => {
const isRemote = isRemoteWorker(extension, data);
const isCloud = data.type === 'cloud';
if (isCloud) {
// For cloud workers, don't show port (it's always 443)
return `<span class="dist-worker-info__title">${data.name}</span><br><span class="dist-worker-info__meta">${data.host}</span>`;
} else if (isRemote) {
const hostLabel = data.host
? `${data.host}:${data.port}`
: `Unconfigured remote worker • Port ${data.port}`;
return `<span class="dist-worker-info__title">${data.name}</span><br><span class="dist-worker-info__meta">${hostLabel}</span>`;
} else {
const cudaInfo = data.cuda_device !== undefined ? `CUDA ${data.cuda_device} • ` : '';
return `<span class="dist-worker-info__title">${data.name}</span><br><span class="dist-worker-info__meta">${cudaInfo}Port ${data.port}</span>`;
}
},
controls: {
dynamic: true
},
settings: {
formType: 'worker',
id: (data) => `settings-${data.id}`,
expandedId: (data) => data?.id
},
hover: true,
expand: true,
border: 'solid'
},
blueprint: {
checkbox: {
type: 'icon',
content: '+',
width: 42,
style: `border-right: 2px dashed ${UI_COLORS.BORDER_LIGHT}; color: ${UI_COLORS.ACCENT_COLOR}; font-size: 24px; font-weight: 500;`
},
statusDot: {
color: 'transparent',
border: `1px solid ${UI_COLORS.BORDER_LIGHT}`
},
infoText: () => `<strong style="color: #aaa; font-size: 16px;">Add New Worker</strong><br><small style="color: ${UI_COLORS.BORDER_LIGHT};">[CUDA] • [Port]</small>`,
controls: {
type: 'ghost',
text: 'Configure',
style: `border: 1px solid ${UI_COLORS.BORDER_DARK}; background: transparent; color: ${UI_COLORS.BORDER_LIGHT};`
},
hover: 'placeholder',
expand: false,
border: 'dashed'
},
add: {
checkbox: {
type: 'icon',
content: '+',
width: 43,
style: `border-right: 1px dashed ${UI_COLORS.BORDER_DARK}; color: ${UI_COLORS.BORDER_LIGHT}; font-size: 18px;`
},
statusDot: {
color: 'transparent',
border: `1px solid ${UI_COLORS.BORDER_LIGHT}`
},
infoText: () => `<span style="color: ${UI_COLORS.ICON_COLOR}; font-weight: bold; font-size: 13px;">Add New Worker</span>`,
controls: null,
hover: 'placeholder',
expand: false,
border: 'dashed',
minHeight: '48px'
}
};
export class DistributedUI {
constructor() {
// UI element styles
this.styles = UI_STYLES;
}
createStatusDot(id, color = "#666", title = "Status") {
const dot = document.createElement("span");
if (id) dot.id = id;
dot.style.cssText = this.styles.statusDot + ` background-color: ${color};`;
dot.title = title;
return dot;
}
createButton(text, onClick, customStyle = "") {
const button = document.createElement("button");
button.textContent = text;
button.className = "distributed-button";
button.style.cssText = BUTTON_STYLES.base + customStyle;
if (onClick) button.onclick = onClick;
return button;
}
createButtonGroup(buttons, style = "") {
const group = document.createElement("div");
group.style.cssText = this.styles.buttonGroup + style;
buttons.forEach(button => group.appendChild(button));
return group;
}
createWorkerControls(workerId, handlers = {}) {
const controlsDiv = document.createElement("div");
controlsDiv.id = `controls-${workerId}`;
controlsDiv.style.cssText = this.styles.controlsDiv;
const buttons = [];
if (handlers.launch) {
const launchBtn = this.createButton('Launch', handlers.launch);
launchBtn.id = `launch-${workerId}`;
launchBtn.title = "Launch this worker instance";
buttons.push(launchBtn);
}
if (handlers.stop) {
const stopBtn = this.createButton('Stop', handlers.stop);
stopBtn.id = `stop-${workerId}`;
stopBtn.title = "Stop this worker instance";
buttons.push(stopBtn);
}
if (handlers.viewLog) {
const logBtn = this.createButton('View Log', handlers.viewLog);
logBtn.id = `log-${workerId}`;
logBtn.title = "View worker log file";
buttons.push(logBtn);
}
buttons.forEach(btn => controlsDiv.appendChild(btn));
return controlsDiv;
}
createFormGroup(label, value, id, type = "text", placeholder = "") {
const group = document.createElement("div");
group.style.cssText = this.styles.formGroup;
const labelEl = document.createElement("label");
labelEl.textContent = label;
labelEl.htmlFor = id;
labelEl.style.cssText = this.styles.formLabel;
const input = document.createElement("input");
input.type = type;
input.id = id;
input.value = value;
input.placeholder = placeholder;
input.classList.add('dist-form-input');
input.style.cssText = this.styles.formInput;
group.appendChild(labelEl);
group.appendChild(input);
return { group, input };
}
createInfoBox(text) {
const box = document.createElement("div");
box.classList.add('dist-info-box');
box.style.cssText = this.styles.infoBox;
box.textContent = text;
return box;
}
addHoverEffect(element, onHover, onLeave) {
element.onmouseover = onHover;
element.onmouseout = onLeave;
}
createCard(type = 'worker', options = {}) {
const card = document.createElement("div");
card.classList.add('dist-card');
switch(type) {
case 'master':
case 'worker':
card.style.cssText = this.styles.workerCard;
break;
case 'blueprint':
card.classList.add('dist-card--blueprint');
card.style.cssText = this.styles.cardBase + this.styles.cardBlueprint;
if (options.onClick) card.onclick = options.onClick;
if (options.title) card.title = options.title;
break;
case 'add':
card.classList.add('dist-card--add');
card.style.cssText = this.styles.cardBase + this.styles.cardAdd;
if (options.onClick) card.onclick = options.onClick;
if (options.title) card.title = options.title;
break;
}
if (options.onMouseEnter) {
card.addEventListener('mouseenter', options.onMouseEnter);
}
if (options.onMouseLeave) {
card.addEventListener('mouseleave', options.onMouseLeave);
}
return card;
}
createCardColumn(type = 'checkbox', options = {}) {
const column = document.createElement("div");
switch(type) {
case 'checkbox':
column.classList.add('dist-card__left-col');
column.style.cssText = this.styles.checkboxColumn;
if (options.title) column.title = options.title;
break;
case 'icon':
column.style.cssText = this.styles.columnBase + this.styles.iconColumn;
break;
case 'content':
column.style.cssText = this.styles.contentColumn;
break;
}
return column;
}
createInfoRow(options = {}) {
const row = document.createElement("div");
row.style.cssText = this.styles.infoRow;
if (options.onClick) row.onclick = options.onClick;
return row;
}
createWorkerContent() {
const content = document.createElement("div");
content.style.cssText = this.styles.workerContent;
return content;
}
createSettingsForm(fields = [], options = {}) {
const form = document.createElement("div");
form.style.cssText = this.styles.settingsForm;
fields.forEach(field => {
if (field.type === 'checkbox') {
const group = document.createElement("div");
group.style.cssText = this.styles.checkboxGroup;
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = field.id;
checkbox.checked = field.checked || false;
if (field.onChange) checkbox.onchange = field.onChange;
const label = document.createElement("label");
label.htmlFor = field.id;
label.textContent = field.label;
label.style.cssText = this.styles.formLabelClickable;
group.appendChild(checkbox);
group.appendChild(label);
form.appendChild(group);
} else {
const result = this.createFormGroup(field.label, field.value, field.id, field.type, field.placeholder);
if (field.groupId) result.group.id = field.groupId;
if (field.display) result.group.style.display = field.display;
form.appendChild(result.group);
}
});
if (options.buttons) {
const buttonGroup = this.createButtonGroup(options.buttons, options.buttonStyle || " margin-top: 8px;");
form.appendChild(buttonGroup);
}
return form;
}
createButtonHelper(text, onClick, style) {
return createButtonHelperFn(this, text, onClick, style);
}
updateMasterDisplay(extension) {
// Use persistent config value as fallback
const cudaDevice = extension?.config?.master?.cuda_device ?? extension?.masterCudaDevice;
// Update CUDA info if element exists
const cudaInfo = document.getElementById('master-cuda-info');
if (cudaInfo) {
const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
if (cudaDevice !== undefined && cudaDevice !== null) {
cudaInfo.textContent = `CUDA ${cudaDevice} • Port ${port}`;
} else {
cudaInfo.textContent = `Port ${port}`;
}
}
// Update name if changed
const nameDisplay = document.getElementById('master-name-display');
if (nameDisplay && extension?.config?.master?.name) {
nameDisplay.textContent = extension.config.master.name;
}
}
showToast(app, severity, summary, detail, life = 3000) {
if (app.extensionManager?.toast?.add) {
app.extensionManager.toast.add({ severity, summary, detail, life });
}
}
showCloudflareWarning(extension, masterHost) {
return showCloudflareWarningFn(extension, masterHost);
}
updateStatusDot(workerId, color, title, pulsing = false) {
const statusDot = document.getElementById(`status-${workerId}`);
if (!statusDot) return;
const statusClasses = [
"worker-status--online",
"worker-status--offline",
"worker-status--unknown",
"worker-status--processing",
];
statusDot.classList.remove(...statusClasses);
const colorClassMap = {
[STATUS_COLORS.ONLINE_GREEN]: "worker-status--online",
[STATUS_COLORS.OFFLINE_RED]: "worker-status--offline",
[STATUS_COLORS.DISABLED_GRAY]: "worker-status--unknown",
[STATUS_COLORS.PROCESSING_YELLOW]: "worker-status--processing",
};
const statusClass = colorClassMap[color] || "worker-status--unknown";
statusDot.classList.add(statusClass);
statusDot.style.backgroundColor = "";
statusDot.title = title;
statusDot.classList.toggle('status-pulsing', pulsing);
}
showLogModal(extension, workerId, logData, fetchLog = null) {
if (this._logModal) {
this._logModal.unmount();
this._logModal = null;
}
const worker = extension.config.workers.find(w => w.id === workerId);
const workerName = worker?.name || `Worker ${workerId}`;
const modal = createLogModal();
this._logModal = modal;
const themeClass =
extension.panelElement?.classList.contains("distributed-panel--light")
? "distributed-panel--light"
: "";
modal.mount(document.body, {
workerName,
logData,
fetchLog: fetchLog || (async () => extension.api.getWorkerLog(workerId, 1000)),
themeClass,
onClose: () => {
if (this._logModal === modal) {
this._logModal = null;
}
},
});
}
createWorkerSettingsForm(extension, worker) {
return createWorkerSettingsFormFn(this, extension, worker);
}
createSettingsToggle() {
const settingsRow = document.createElement("div");
settingsRow.style.cssText = this.styles.settingsToggle;
const settingsTitle = document.createElement("h4");
settingsTitle.textContent = "Settings";
settingsTitle.style.cssText = "margin: 0; font-size: 14px;";
const settingsToggle = document.createElement("span");
settingsToggle.textContent = "▶"; // Right arrow when collapsed
settingsToggle.style.cssText =
"font-size: 12px; color: var(--dist-settings-arrow, #888); transition: all 0.2s ease;";
settingsRow.appendChild(settingsToggle);
settingsRow.appendChild(settingsTitle);
return { settingsRow, settingsToggle };
}
createCheckboxOrIconColumn(config, data, extension) {
const column = this.createCardColumn('checkbox');
if (config?.type === 'icon') {
column.style.flex = `0 0 ${config.width || 44}px`;
column.innerHTML = config.content || '+';
if (config.style) {
const styles = config.style.split(';').filter(s => s.trim());
styles.forEach(style => {
const [prop, value] = style.split(':').map(s => s.trim());
if (prop && value) {
column.style[prop.replace(/-([a-z])/g, (g) => g[1].toUpperCase())] = value;
}
});
}
} else {
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = `gpu-${data?.id || 'master'}`;
checkbox.checked = config?.checked !== undefined ? config.checked : data?.enabled;
checkbox.disabled = config?.disabled || false;
checkbox.style.cssText = `cursor: ${config?.disabled ? 'default' : 'pointer'}; width: 16px; height: 16px;`;
if (config?.opacity) checkbox.style.opacity = config.opacity;
if (config?.title) column.title = config.title;
const isMasterToggle = config?.masterToggle && typeof extension.isMasterParticipating === 'function';
if (isMasterToggle) {
const participationEnabled = extension.isMasterParticipationEnabled();
const fallbackActive = extension.isMasterFallbackActive();
const buildTitle = (enabled, fallback) => {
if (enabled) {
return "Master participating • Click to switch to orchestrator-only";
}
if (fallback) {
return "No workers selected • Master fallback execution active";
}
return "Master orchestrator-only • Click to re-enable participation";
};
checkbox.checked = participationEnabled;
checkbox.style.pointerEvents = "none";
column.style.cursor = "pointer";
column.title = buildTitle(participationEnabled, fallbackActive);
column.onclick = async (event) => {
if (event) {
event.stopPropagation();
event.preventDefault();
}
const nextState = !extension.isMasterParticipationEnabled();
const nextFallback = !nextState && extension.enabledWorkers.length === 0;
checkbox.checked = nextState;
column.title = buildTitle(nextState, nextFallback);
await extension.updateMasterParticipation(nextState);
};
} else if (config?.enabled && !config?.disabled && data?.id) {
checkbox.style.pointerEvents = "none";
column.style.cursor = "pointer";
column.onclick = async () => {
checkbox.checked = !checkbox.checked;
await extension.updateWorkerEnabled(data.id, checkbox.checked);
};
}
column.appendChild(checkbox);
}
return column;
}
createStatusDotHelper(config, data, extension) {
let color = config.color || "#666";
let title = config.title || "Status";
let id = config.id;
if (typeof config.initialColor === 'function') {
color = config.initialColor(data, extension);
}
if (typeof config.initialTitle === 'function') {
title = config.initialTitle(data, extension);
}
if (typeof config.id === 'function') {
id = config.id(data);
}
const dot = this.createStatusDot(id, color, title);
if (config.border) {
dot.style.border = config.border;
}
if (config.pulsing && (typeof config.pulsing !== 'function' || config.pulsing(data))) {
dot.classList.add('status-pulsing');
}
return dot;
}
createSettingsToggleHelper(expandedId, extension) {
const arrow = document.createElement("span");
arrow.className = "settings-arrow";
arrow.innerHTML = "▶";
arrow.style.cssText = this.styles.settingsArrow;
const isExpanded = typeof expandedId === 'function' ?
extension.state.isWorkerExpanded(expandedId(extension)) :
(expandedId === 'master' ? false : extension.state.isWorkerExpanded(expandedId));
if (isExpanded) {
arrow.style.transform = "rotate(90deg)";
}
return arrow;
}
createControlsSection(config, data, extension, isRemote) {
if (!config) return null;
const controlsDiv = document.createElement("div");
controlsDiv.id = `controls-${data?.id || 'master'}`;
controlsDiv.style.cssText = this.styles.controlsDiv;
// Always create a wrapper div for consistent layout
const controlsWrapper = document.createElement("div");
controlsWrapper.style.cssText = this.styles.controlsWrapper;
if (config.type === 'master') {
const participationEnabled = extension.isMasterParticipationEnabled();
const fallbackActive = extension.isMasterFallbackActive();
let message;
const badge = document.createElement("div");
badge.classList.add("dist-info-box", "master-info-badge");
badge.style.cssText = this.styles.infoBox;
if (fallbackActive) {
message = "No workers selected. Master fallback execution active.";
badge.textContent = message;
badge.classList.add("master-info-badge--fallback");
} else if (!participationEnabled) {
message = "Master disabled: running as orchestrator only";
badge.textContent = message;
badge.classList.add("master-info-badge--delegate");
} else {
message = "Master participating in workflows";
badge.textContent = message;
}
controlsWrapper.appendChild(badge);
} else if (config.dynamic && data) {
if (isRemote) {
const isCloud = data.type === 'cloud';
const workerTypeText = isCloud ? "Cloud worker" : "Remote worker";
const workerTypeBadge = this.createInfoBox(workerTypeText);
workerTypeBadge.title = "Worker is externally hosted";
controlsWrapper.appendChild(workerTypeBadge);
const logBtn = this.createButton('View Log', () => viewWorkerLog(extension, data.id, true));
logBtn.id = `log-${data.id}`;
logBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl;
logBtn.classList.add("btn--log");
logBtn.title = "View remote worker log";
controlsWrapper.appendChild(logBtn);
} else {
const controls = this.createWorkerControls(data.id, {
launch: () => launchWorker(extension, data.id),
stop: () => stopWorker(extension, data.id),
viewLog: () => viewWorkerLog(extension, data.id)
});
const launchBtn = controls.querySelector(`#launch-${data.id}`);
const stopBtn = controls.querySelector(`#stop-${data.id}`);
const logBtn = controls.querySelector(`#log-${data.id}`);
launchBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl;
launchBtn.classList.add("btn--launch");
launchBtn.title = "Launch worker (runs in background with logging)";
stopBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl + BUTTON_STYLES.hidden;
stopBtn.classList.add("btn--stop");
stopBtn.title = "Stop worker";
logBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl + BUTTON_STYLES.hidden;
logBtn.classList.add("btn--log");
while (controls.firstChild) {
controlsWrapper.appendChild(controls.firstChild);
}
}
} else if (config.type === 'info') {
const infoBtn = this.createButton(config.text, null, config.style || "");
infoBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl + (config.style || BUTTON_STYLES.info) + " cursor: default;";
infoBtn.disabled = true;
controlsWrapper.appendChild(infoBtn);
} else if (config.type === 'ghost') {
const ghostBtn = document.createElement("button");
ghostBtn.style.cssText = `flex: 1; padding: 5px 14px; font-size: 11px; font-weight: 500; border-radius: 4px; cursor: default; ${config.style || ""}`;
ghostBtn.textContent = config.text;
ghostBtn.disabled = true;
controlsWrapper.appendChild(ghostBtn);
}
controlsDiv.appendChild(controlsWrapper);
return controlsDiv;
}
createSettingsSection(config, data, extension) {
const settingsDiv = document.createElement("div");
const settingsId = typeof config.id === 'function' ? config.id(data) : config.id;
settingsDiv.id = settingsId;
settingsDiv.className = "worker-settings";
const expandedId = typeof config.expandedId === 'function' ? config.expandedId(data) : config.expandedId;
const isExpanded = expandedId === 'master-settings' ? false : extension.state.isWorkerExpanded(expandedId);
settingsDiv.style.cssText = this.styles.workerSettings;
if (isExpanded) {
settingsDiv.classList.add("expanded");
settingsDiv.style.padding = "12px";
settingsDiv.style.marginTop = "8px";
settingsDiv.style.marginBottom = "8px";
}
let settingsForm;
if (config.formType === 'master') {
settingsForm = this.createMasterSettingsForm(extension, data);
} else if (config.formType === 'worker') {
settingsForm = this.createWorkerSettingsForm(extension, data);
}
if (settingsForm) {
settingsDiv.appendChild(settingsForm);
}
return settingsDiv;
}
createMasterSettingsForm(extension, data) {
const settingsForm = document.createElement("div");
settingsForm.style.cssText = "display: flex; flex-direction: column; gap: 8px;";
const nameResult = this.createFormGroup("Name:", extension.config?.master?.name || "Master", "master-name");
settingsForm.appendChild(nameResult.group);
const hostResult = this.createFormGroup("Host:", extension.config?.master?.host || "", "master-host", "text", "Auto-detect if empty");
settingsForm.appendChild(hostResult.group);
// Cloudflare tunnel toggle (simple button inside master settings)
const tunnelBtn = this.createButton("Enable Cloudflare Tunnel", (e) => extension.handleTunnelToggle(e.target));
tunnelBtn.id = "cloudflare-tunnel-button";
tunnelBtn.style.cssText = BUTTON_STYLES.base + " margin: 4px 0 -5px 0;";
tunnelBtn.classList.add("tunnel-button", "tunnel-button--enable");
settingsForm.appendChild(tunnelBtn);
extension.tunnelElements = { button: tunnelBtn };
extension.updateTunnelUIElements();
const saveBtn = this.createButton("Save", async () => {
const nameInput = document.getElementById('master-name');
const hostInput = document.getElementById('master-host');
if (!extension.config.master) extension.config.master = {};
extension.config.master.name = nameInput.value.trim() || "Master";
const hostValue = hostInput.value.trim();
await extension.api.updateMaster({
host: hostValue,
name: extension.config.master.name
});
// Reload config to refresh any updated values
await extension.loadConfig();
// If host was emptied, trigger auto-detection
if (!hostValue) {
extension.log("Host field cleared, triggering IP auto-detection", "debug");
await extension.detectMasterIP();
// Reload config again to get the auto-detected IP
await extension.loadConfig();
// Update the input field with the detected IP
document.getElementById('master-host').value = extension.config?.master?.host || "";
}
document.getElementById('master-name-display').textContent = extension.config.master.name;
this.updateMasterDisplay(extension);
// Show toast notification
if (extension.app?.extensionManager?.toast) {
const message = !hostValue ?
"Master settings saved and IP auto-detected" :
"Master settings saved successfully";
extension.app.extensionManager.toast.add({
severity: "success",
summary: "Master Updated",
detail: message,
life: 3000
});
}
saveBtn.textContent = "Saved!";
setTimeout(() => { saveBtn.textContent = "Save"; }, TIMEOUTS.FLASH_LONG);
}, "background-color: #4a7c4a;");
saveBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.success;
const cancelBtn = this.createButton("Cancel", () => {
document.getElementById('master-name').value = extension.config?.master?.name || "Master";
document.getElementById('master-host').value = extension.config?.master?.host || "";
}, "background-color: #555;");
cancelBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.cancel;
const buttonGroup = this.createButtonGroup([saveBtn, cancelBtn], " margin-top: 8px;");
settingsForm.appendChild(buttonGroup);
return settingsForm;
}
addPlaceholderHover(card, leftColumn, entityType) {
const cardTypeClass = entityType === 'blueprint' ? 'placeholder-card--blueprint' : 'placeholder-card--add';
const columnTypeClass = entityType === 'blueprint' ? 'placeholder-column--blueprint' : 'placeholder-column--add';
card.classList.add('placeholder-card', cardTypeClass);
leftColumn.classList.add('placeholder-column', columnTypeClass);
card.onmouseover = () => {
card.classList.add('is-hovered');
leftColumn.classList.add('is-hovered');
};
card.onmouseout = () => {
card.classList.remove('is-hovered');
leftColumn.classList.remove('is-hovered');
};
}
renderEntityCard(entityType, data, extension) {
return renderEntityCardFn(this, cardConfigs, entityType, data, extension);
}
}