feature/IO-3587-Commision-Cut - Improved localstack client
This commit is contained in:
@@ -594,10 +594,10 @@ function renderHtml() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="heroTopRow">
|
<div class="heroTopRow">
|
||||||
<div id="workspaceTabs" class="workspaceTabs" role="tablist" aria-label="Inspector views">
|
<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 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" type="button" data-panel="logs" aria-pressed="false">📜 CloudWatch Logs</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -605,7 +605,7 @@ function renderHtml() {
|
|||||||
<section id="emailsPanel" class="workspacePanel">
|
<section id="emailsPanel" class="workspacePanel">
|
||||||
<section class="toolControls">
|
<section class="toolControls">
|
||||||
<div class="row">
|
<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"><input id="autoToggle" type="checkbox" checked> Live refresh</label>
|
||||||
<label class="chip">Every
|
<label class="chip">Every
|
||||||
<select id="intervalSelect">
|
<select id="intervalSelect">
|
||||||
@@ -648,7 +648,7 @@ function renderHtml() {
|
|||||||
<section id="logsPanel" class="workspacePanel" hidden>
|
<section id="logsPanel" class="workspacePanel" hidden>
|
||||||
<section class="toolControls">
|
<section class="toolControls">
|
||||||
<div class="row">
|
<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"><input id="logsAutoToggle" type="checkbox" checked> Live refresh</label>
|
||||||
<label class="chip">Every
|
<label class="chip">Every
|
||||||
<select id="logsIntervalSelect">
|
<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}
|
.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}
|
.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)}
|
.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{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)}
|
.workspaceTab.active{background:#fff;border-color:rgba(31,41,51,.08);color:var(--ink)}
|
||||||
.themeToggle{white-space:nowrap}
|
.themeToggle{white-space:nowrap}
|
||||||
@@ -1083,12 +1083,12 @@ function clientApp(config) {
|
|||||||
|
|
||||||
el.expandAllButton.addEventListener("click", () => {
|
el.expandAllButton.addEventListener("click", () => {
|
||||||
state.filtered.forEach((message) => state.openIds.add(message.id));
|
state.filtered.forEach((message) => state.openIds.add(message.id));
|
||||||
renderList();
|
syncCardExpansion();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.collapseAllButton.addEventListener("click", () => {
|
el.collapseAllButton.addEventListener("click", () => {
|
||||||
state.openIds.clear();
|
state.openIds.clear();
|
||||||
renderList();
|
syncCardExpansion();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.scrollToTopButton.addEventListener("click", () => {
|
el.scrollToTopButton.addEventListener("click", () => {
|
||||||
@@ -1825,7 +1825,7 @@ function clientApp(config) {
|
|||||||
el.empty.hidden = true;
|
el.empty.hidden = true;
|
||||||
el.list.innerHTML = state.filtered.map(renderCard).join("");
|
el.list.innerHTML = state.filtered.map(renderCard).join("");
|
||||||
bindCardToggles();
|
bindCardToggles();
|
||||||
el.list.querySelectorAll(".card[open]").forEach((details) => hydrate(details, getMessage(details.dataset.id)));
|
syncCardExpansion();
|
||||||
updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton);
|
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>
|
<span class="tag logTag" title="${escapeHtml(event.logStreamName || "Unknown stream")}">${escapeHtml(event.logStreamName || "Unknown stream")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="logSummaryActions">
|
<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>
|
<span class="tag">${escapeHtml(formatRelative(event.timestamp || Date.now()))}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1919,7 +1919,6 @@ function clientApp(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderCard(message) {
|
function renderCard(message) {
|
||||||
const open = state.openIds.has(message.id);
|
|
||||||
const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text");
|
const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text");
|
||||||
const tags = [];
|
const tags = [];
|
||||||
|
|
||||||
@@ -1940,7 +1939,7 @@ function clientApp(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `
|
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">
|
<summary class="summary">
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<div class="head">
|
<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>
|
<button class="tab ${view === "raw" ? "active" : ""}" type="button" data-action="view" data-view="raw" data-id="${escapeHtml(message.id)}">Raw</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1985,7 +1984,8 @@ function clientApp(config) {
|
|||||||
? `<div class="attachments">${message.attachments
|
? `<div class="attachments">${message.attachments
|
||||||
.map((attachment) => {
|
.map((attachment) => {
|
||||||
const size = attachment.size ? `, ${formatBytes(attachment.size)}` : "";
|
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>`
|
.join("")}</div>`
|
||||||
: ""
|
: ""
|
||||||
@@ -1999,7 +1999,7 @@ function clientApp(config) {
|
|||||||
|
|
||||||
function renderPanel(message, view) {
|
function renderPanel(message, view) {
|
||||||
if (view === "rendered" && message.hasHtml) {
|
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") {
|
if (view === "raw") {
|
||||||
@@ -2027,6 +2027,88 @@ function clientApp(config) {
|
|||||||
return `<div class="metaCard"><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`;
|
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) {
|
function hydrate(details, message) {
|
||||||
if (!details || !details.open || !message) {
|
if (!details || !details.open || !message) {
|
||||||
return;
|
return;
|
||||||
@@ -2155,7 +2237,7 @@ function clientApp(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDark = appState.theme === "dark";
|
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-pressed", isDark ? "true" : "false");
|
||||||
el.themeToggle.setAttribute("aria-label", isDark ? "Switch to light theme" : "Switch to dark theme");
|
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";
|
el.themeToggle.title = isDark ? "Switch to light theme" : "Switch to dark theme";
|
||||||
|
|||||||
Reference in New Issue
Block a user