feature/IO-3587-Commision-Cut - Improved localstack client

This commit is contained in:
Dave
2026-03-19 17:14:32 -04:00
parent f0f5c09fd7
commit 31c6e7c0e5

View File

@@ -594,10 +594,10 @@ function renderHtml() {
</div>
<div class="heroTopRow">
<div id="workspaceTabs" class="workspaceTabs" role="tablist" aria-label="Inspector views">
<button class="workspaceTab active" type="button" data-panel="emails" aria-pressed="true">SES Emails</button>
<button class="workspaceTab" type="button" data-panel="logs" aria-pressed="false">CloudWatch Logs</button>
<button class="workspaceTab active" type="button" data-panel="emails" aria-pressed="true">✉️ SES Emails</button>
<button class="workspaceTab" type="button" data-panel="logs" aria-pressed="false">📜 CloudWatch Logs</button>
</div>
<button id="themeToggle" class="ghost themeToggle" type="button" aria-pressed="false">Theme: Light</button>
<button id="themeToggle" class="ghost themeToggle" type="button" aria-pressed="false">☀️ Light theme</button>
</div>
</div>
</header>
@@ -605,7 +605,7 @@ function renderHtml() {
<section id="emailsPanel" class="workspacePanel">
<section class="toolControls">
<div class="row">
<button id="refreshButton" class="primary" type="button">Refresh</button>
<button id="refreshButton" class="primary" type="button">🔄 Refresh</button>
<label class="chip"><input id="autoToggle" type="checkbox" checked> Live refresh</label>
<label class="chip">Every
<select id="intervalSelect">
@@ -648,7 +648,7 @@ function renderHtml() {
<section id="logsPanel" class="workspacePanel" hidden>
<section class="toolControls">
<div class="row">
<button id="logsRefreshButton" class="primary" type="button">Refresh</button>
<button id="logsRefreshButton" class="primary" type="button">🔄 Refresh</button>
<label class="chip"><input id="logsAutoToggle" type="checkbox" checked> Live refresh</label>
<label class="chip">Every
<select id="logsIntervalSelect">
@@ -741,7 +741,7 @@ function renderStyles() {
.heroTopRow{display:flex;flex:1 1 360px;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end}
.helper{margin:0;color:var(--muted);font-size:.89rem}
.workspaceTabs{display:flex;flex-wrap:wrap;gap:6px;padding:4px;border-radius:999px;background:rgba(31,41,51,.05)}
.workspaceTab,.primary,.ghost,.mini,.tab{border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease}
.workspaceTab,.primary,.ghost,.mini,.tab{display:inline-flex;align-items:center;justify-content:center;gap:6px;border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease}
.workspaceTab{min-height:32px;padding:0 12px;background:transparent;color:var(--muted);font-weight:700}
.workspaceTab.active{background:#fff;border-color:rgba(31,41,51,.08);color:var(--ink)}
.themeToggle{white-space:nowrap}
@@ -1083,12 +1083,12 @@ function clientApp(config) {
el.expandAllButton.addEventListener("click", () => {
state.filtered.forEach((message) => state.openIds.add(message.id));
renderList();
syncCardExpansion();
});
el.collapseAllButton.addEventListener("click", () => {
state.openIds.clear();
renderList();
syncCardExpansion();
});
el.scrollToTopButton.addEventListener("click", () => {
@@ -1825,7 +1825,7 @@ function clientApp(config) {
el.empty.hidden = true;
el.list.innerHTML = state.filtered.map(renderCard).join("");
bindCardToggles();
el.list.querySelectorAll(".card[open]").forEach((details) => hydrate(details, getMessage(details.dataset.id)));
syncCardExpansion();
updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton);
}
@@ -1867,7 +1867,7 @@ function clientApp(config) {
<span class="tag logTag" title="${escapeHtml(event.logStreamName || "Unknown stream")}">${escapeHtml(event.logStreamName || "Unknown stream")}</span>
</div>
<div class="logSummaryActions">
<button class="mini logCopyButton" type="button" data-log-action="copy" data-id="${escapeHtml(event.id)}">Copy JSON</button>
<button class="mini logCopyButton" type="button" data-log-action="copy" data-id="${escapeHtml(event.id)}">📋 Copy JSON</button>
<span class="tag">${escapeHtml(formatRelative(event.timestamp || Date.now()))}</span>
</div>
</div>
@@ -1919,7 +1919,6 @@ function clientApp(config) {
}
function renderCard(message) {
const open = state.openIds.has(message.id);
const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text");
const tags = [];
@@ -1940,7 +1939,7 @@ function clientApp(config) {
}
return `
<details class="card ${state.newIds.has(message.id) ? "new" : ""}" data-id="${escapeHtml(message.id)}" ${open ? "open" : ""}>
<details class="card ${state.newIds.has(message.id) ? "new" : ""}" data-id="${escapeHtml(message.id)}">
<summary class="summary">
<div class="top">
<div class="head">
@@ -1964,7 +1963,7 @@ function clientApp(config) {
<button class="tab ${view === "raw" ? "active" : ""}" type="button" data-action="view" data-view="raw" data-id="${escapeHtml(message.id)}">Raw</button>
</div>
<div class="actions">
<button class="mini" type="button" data-action="copy-raw" data-id="${escapeHtml(message.id)}">Copy raw</button>
<button class="mini" type="button" data-action="copy-raw" data-id="${escapeHtml(message.id)}">📋 Copy raw</button>
</div>
</div>
@@ -1985,7 +1984,8 @@ function clientApp(config) {
? `<div class="attachments">${message.attachments
.map((attachment) => {
const size = attachment.size ? `, ${formatBytes(attachment.size)}` : "";
return `<a class="attachment attachmentLink" href="/api/messages/${encodeURIComponent(message.id)}/attachments/${attachment.index}" download="${escapeHtml(attachment.filename)}" title="Download ${escapeHtml(attachment.filename)}">${escapeHtml(attachment.filename)} (${escapeHtml(attachment.contentType)}${escapeHtml(size)})</a>`;
const icon = resolveAttachmentIcon(attachment);
return `<a class="attachment attachmentLink" href="/api/messages/${encodeURIComponent(message.id)}/attachments/${attachment.index}" download="${escapeHtml(attachment.filename)}" title="Download ${escapeHtml(attachment.filename)}">${icon} ${escapeHtml(attachment.filename)} (${escapeHtml(attachment.contentType)}${escapeHtml(size)})</a>`;
})
.join("")}</div>`
: ""
@@ -1999,7 +1999,7 @@ function clientApp(config) {
function renderPanel(message, view) {
if (view === "rendered" && message.hasHtml) {
return `<iframe title="${escapeHtml(message.subject)}" data-frame loading="lazy"></iframe>`;
return `<iframe title="${escapeHtml(message.subject)}" data-frame sandbox="" referrerpolicy="no-referrer"></iframe>`;
}
if (view === "raw") {
@@ -2027,6 +2027,88 @@ function clientApp(config) {
return `<div class="metaCard"><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`;
}
function syncCardExpansion() {
const applyCardState = () => {
el.list.querySelectorAll(".card").forEach((details) => {
const id = details.dataset.id;
const shouldOpen = Boolean(id && state.openIds.has(id));
if (shouldOpen && !details.open) {
details.open = true;
return;
}
if (!shouldOpen && details.open) {
details.open = false;
return;
}
if (shouldOpen) {
hydrate(details, getMessage(id));
}
});
};
applyCardState();
window.requestAnimationFrame(applyCardState);
}
function resolveAttachmentIcon(attachment) {
const filename = String(attachment?.filename || "").toLowerCase();
const contentType = String(attachment?.contentType || "").toLowerCase();
if (filename.endsWith(".pdf") || contentType.includes("pdf")) {
return "📄";
}
if (
[".doc", ".docx", ".txt", ".rtf", ".md"].some((extension) => filename.endsWith(extension)) ||
contentType.includes("word") ||
contentType.startsWith("text/")
) {
return "📝";
}
if (
[".xls", ".xlsx", ".csv"].some((extension) => filename.endsWith(extension)) ||
contentType.includes("sheet") ||
contentType.includes("csv")
) {
return "📊";
}
if (
filename.endsWith(".json") ||
filename.endsWith(".xml") ||
filename.endsWith(".yaml") ||
filename.endsWith(".yml")
) {
return "🧾";
}
if (contentType.startsWith("image/")) {
return "🖼️";
}
if (contentType.startsWith("audio/")) {
return "🎵";
}
if (contentType.startsWith("video/")) {
return "🎬";
}
if (
[".zip", ".rar", ".7z", ".tar", ".gz"].some((extension) => filename.endsWith(extension)) ||
contentType.includes("zip") ||
contentType.includes("compressed")
) {
return "🗜️";
}
return "📎";
}
function hydrate(details, message) {
if (!details || !details.open || !message) {
return;
@@ -2155,7 +2237,7 @@ function clientApp(config) {
}
const isDark = appState.theme === "dark";
el.themeToggle.textContent = isDark ? "Theme: Dark" : "Theme: Light";
el.themeToggle.textContent = isDark ? "🌙 Dark theme" : "☀️ Light theme";
el.themeToggle.setAttribute("aria-pressed", isDark ? "true" : "false");
el.themeToggle.setAttribute("aria-label", isDark ? "Switch to light theme" : "Switch to dark theme");
el.themeToggle.title = isDark ? "Switch to light theme" : "Switch to dark theme";