feature/IO-3587-Commision-Cut - Improved localstack client
This commit is contained in:
@@ -689,6 +689,8 @@ function renderHtml() {
|
||||
<div class="row">
|
||||
<input id="logsSearchInput" class="search" type="search" placeholder="Search stream name or log content..." autocomplete="off">
|
||||
<button id="logsClearSearchButton" class="ghost" type="button">Clear</button>
|
||||
<button id="logsExpandAllButton" class="ghost" type="button">Open all</button>
|
||||
<button id="logsCollapseAllButton" class="ghost" type="button">Close all</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -721,11 +723,11 @@ function renderStyles() {
|
||||
return `
|
||||
:root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);}
|
||||
*{box-sizing:border-box}
|
||||
html,body{margin:0;min-height:100%}
|
||||
html,body{margin:0;height:100%;overflow:hidden}
|
||||
body{color-scheme:light;background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;transition:background-color .18s ease,color .18s ease}
|
||||
button,input,select,textarea{font:inherit}
|
||||
button{cursor:pointer}
|
||||
.page{display:grid;gap:10px;max-width:1360px;align-content:start;margin:0 auto;padding:14px}
|
||||
.page{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;max-width:1360px;height:100vh;height:100dvh;margin:0 auto;padding:14px;overflow:hidden}
|
||||
.hero{display:block;margin-bottom:0}
|
||||
.heroShell,.toolControls,.stat{background:var(--panel);backdrop-filter:blur(14px);border:1px solid var(--line);box-shadow:var(--shadow)}
|
||||
.card{background:var(--card-shell);border:1px solid var(--card-line);box-shadow:var(--card-shadow)}
|
||||
@@ -743,10 +745,10 @@ function renderStyles() {
|
||||
.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}
|
||||
.workspacePanel{display:grid;gap:6px}
|
||||
.workspacePanel{display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:6px;min-height:0}
|
||||
.workspacePanel[hidden]{display:none}
|
||||
.toolControls{display:grid;gap:8px}
|
||||
.contentPane{height:clamp(360px,50vh,720px);overflow:auto;scroll-behavior:smooth;padding-right:4px}
|
||||
.contentPane{height:100%;min-height:0;overflow:auto;scroll-behavior:smooth;padding-right:4px}
|
||||
.contentStack{display:grid;gap:8px;min-width:100%;padding-bottom:18px}
|
||||
.paneTopWrap{display:flex;justify-content:flex-end;position:sticky;bottom:14px;pointer-events:none;padding-right:10px}
|
||||
.paneTopButton{display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;border-radius:999px;border:1px solid rgba(255,255,255,.32);background:rgba(31,41,51,.42);color:#fff;font-size:1.1rem;line-height:1;backdrop-filter:blur(12px);box-shadow:0 10px 24px rgba(31,41,51,.18);opacity:0;transform:translateY(8px);visibility:hidden;pointer-events:none;transition:opacity .16s ease,transform .16s ease,background-color .12s ease;z-index:6}
|
||||
@@ -810,12 +812,17 @@ function renderStyles() {
|
||||
.logSummary::-webkit-details-marker{display:none}
|
||||
.logSummaryTop{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center}
|
||||
.logMeta{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||||
.logSummaryActions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end}
|
||||
.logTag{background:var(--info-soft);color:var(--info);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.logPreview{margin:0;color:#324150;font:600 .88rem/1.45 "Cascadia Code","Consolas",monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.logBody{padding:8px 12px 12px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)}
|
||||
.logCodeWrap{position:relative}
|
||||
.logCopyButton{position:absolute;top:10px;right:10px;z-index:1;backdrop-filter:blur(12px);box-shadow:0 8px 18px rgba(31,41,51,.16)}
|
||||
.logBody pre{border-radius:12px;border:1px solid rgba(48,113,169,.14);padding:42px 12px 12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff}
|
||||
.logCopyButton{box-shadow:none}
|
||||
.logBody pre{border-radius:12px;border:1px solid rgba(48,113,169,.14);padding:12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff}
|
||||
.jsonSyntax .jsonKey{color:#b55f2d}
|
||||
.jsonSyntax .jsonString{color:#1f8f65}
|
||||
.jsonSyntax .jsonNumber{color:#2f6ea9}
|
||||
.jsonSyntax .jsonBoolean{color:#9d5f00}
|
||||
.jsonSyntax .jsonNull{color:#b33a3a}
|
||||
iframe{width:100%;min-height:560px;border:none;background:#fff}
|
||||
pre{margin:0;padding:12px;white-space:pre-wrap;word-break:break-word;overflow:auto;font:12.5px/1.45 "Cascadia Code","Consolas",monospace;color:#102030;background:linear-gradient(180deg,rgba(207,109,60,.04),transparent 140px),#fff}
|
||||
.placeholder,.inlineError{padding:12px}
|
||||
@@ -865,6 +872,11 @@ function renderStyles() {
|
||||
body[data-theme="dark"] .head h2,
|
||||
body[data-theme="dark"] .stat strong,
|
||||
body[data-theme="dark"] h1{color:#edf2f7}
|
||||
body[data-theme="dark"] .jsonSyntax .jsonKey{color:#f0b08a}
|
||||
body[data-theme="dark"] .jsonSyntax .jsonString{color:#80d5b0}
|
||||
body[data-theme="dark"] .jsonSyntax .jsonNumber{color:#94c9ff}
|
||||
body[data-theme="dark"] .jsonSyntax .jsonBoolean{color:#f0c274}
|
||||
body[data-theme="dark"] .jsonSyntax .jsonNull{color:#ff9c9c}
|
||||
body[data-theme="dark"] .meta,
|
||||
body[data-theme="dark"] .helper,
|
||||
body[data-theme="dark"] .lede,
|
||||
@@ -876,7 +888,7 @@ function renderStyles() {
|
||||
body[data-theme="dark"] .paneTopButton{border-color:rgba(255,255,255,.18);background:rgba(8,12,18,.58);color:#edf2f7}
|
||||
body[data-theme="dark"] .paneTopButton.visible:hover{background:rgba(8,12,18,.8)}
|
||||
@media (max-width:1080px){.stats{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media (max-width:720px){.page{padding:12px}.heroShell,.heroTopRow,.toolbar,.row{align-items:stretch}.stats{grid-template-columns:1fr}.heroTopRow{justify-content:stretch;flex-basis:100%}.workspaceTab,.primary,.ghost,.chip,.themeToggle{width:100%;justify-content:center}.logSummaryTop{align-items:flex-start}.logCopyButton{top:8px;right:8px}.contentPane{height:clamp(300px,48vh,560px)}iframe{min-height:420px}}
|
||||
@media (max-width:720px){.page{padding:12px}.heroShell,.heroTopRow,.toolbar,.row{align-items:stretch}.stats{grid-template-columns:1fr}.heroTopRow{justify-content:stretch;flex-basis:100%}.workspaceTab,.primary,.ghost,.chip,.themeToggle{width:100%;justify-content:center}.logSummaryTop,.logSummaryActions{align-items:flex-start}.contentPane{min-height:300px}iframe{min-height:420px}}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -890,19 +902,28 @@ function escapeHtml(value) {
|
||||
}
|
||||
|
||||
function clientApp(config) {
|
||||
const THEME_STORAGE_KEY = "localstack-inspector-theme";
|
||||
const PANEL_STORAGE_KEY = "localstack-inspector-panel";
|
||||
const EMAIL_PREFERENCES_STORAGE_KEY = "localstack-inspector-email-preferences";
|
||||
const LOG_PREFERENCES_STORAGE_KEY = "localstack-inspector-log-preferences";
|
||||
const REFRESH_INTERVALS = [5000, 10000, 15000, 30000, 60000];
|
||||
const LOG_WINDOWS = [300000, 900000, 3600000, 21600000, 86400000];
|
||||
const LOG_LIMITS = [100, 200, 300, 500];
|
||||
const storedEmailPreferences = getStoredPreferences(EMAIL_PREFERENCES_STORAGE_KEY);
|
||||
const storedLogPreferences = getStoredPreferences(LOG_PREFERENCES_STORAGE_KEY);
|
||||
|
||||
const appState = {
|
||||
panel: "emails",
|
||||
panel: getInitialPanel(),
|
||||
logsReady: false,
|
||||
theme: getInitialTheme()
|
||||
};
|
||||
const THEME_STORAGE_KEY = "localstack-inspector-theme";
|
||||
|
||||
const state = {
|
||||
messages: [],
|
||||
filtered: [],
|
||||
search: "",
|
||||
auto: true,
|
||||
interval: config.defaultRefreshMs,
|
||||
search: getStoredText(storedEmailPreferences?.search),
|
||||
auto: getStoredBoolean(storedEmailPreferences?.auto, true),
|
||||
interval: getStoredNumber(storedEmailPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs),
|
||||
loading: false,
|
||||
error: "",
|
||||
updatedAt: 0,
|
||||
@@ -923,13 +944,13 @@ function clientApp(config) {
|
||||
streams: [],
|
||||
events: [],
|
||||
filtered: [],
|
||||
group: config.defaultLogGroup || "",
|
||||
stream: "",
|
||||
search: "",
|
||||
auto: true,
|
||||
interval: config.defaultRefreshMs,
|
||||
windowMs: config.defaultLogWindowMs,
|
||||
limit: config.defaultLogLimit,
|
||||
group: getStoredText(storedLogPreferences?.group, config.defaultLogGroup || ""),
|
||||
stream: getStoredText(storedLogPreferences?.stream),
|
||||
search: getStoredText(storedLogPreferences?.search),
|
||||
auto: getStoredBoolean(storedLogPreferences?.auto, true),
|
||||
interval: getStoredNumber(storedLogPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs),
|
||||
windowMs: getStoredNumber(storedLogPreferences?.windowMs, LOG_WINDOWS, config.defaultLogWindowMs),
|
||||
limit: getStoredNumber(storedLogPreferences?.limit, LOG_LIMITS, config.defaultLogLimit),
|
||||
loading: false,
|
||||
error: "",
|
||||
updatedAt: 0,
|
||||
@@ -938,7 +959,6 @@ function clientApp(config) {
|
||||
newest: 0,
|
||||
searchedLogStreams: 0,
|
||||
openIds: new Set(),
|
||||
hasInteracted: false,
|
||||
listSignature: ""
|
||||
};
|
||||
|
||||
@@ -976,6 +996,8 @@ function clientApp(config) {
|
||||
logsLimitSelect: document.getElementById("logsLimitSelect"),
|
||||
logsSearchInput: document.getElementById("logsSearchInput"),
|
||||
logsClearSearchButton: document.getElementById("logsClearSearchButton"),
|
||||
logsExpandAllButton: document.getElementById("logsExpandAllButton"),
|
||||
logsCollapseAllButton: document.getElementById("logsCollapseAllButton"),
|
||||
logsScrollToTopButton: document.getElementById("logsScrollToTopButton"),
|
||||
logsStatusChip: document.getElementById("logsStatusChip"),
|
||||
logsTotalStat: document.getElementById("logsTotalStat"),
|
||||
@@ -991,16 +1013,27 @@ function clientApp(config) {
|
||||
logsContentPane: document.getElementById("logsContentPane")
|
||||
};
|
||||
|
||||
el.intervalSelect.value = String(config.defaultRefreshMs);
|
||||
el.logsIntervalSelect.value = String(config.defaultRefreshMs);
|
||||
el.logsWindowSelect.value = String(config.defaultLogWindowMs);
|
||||
el.logsLimitSelect.value = String(config.defaultLogLimit);
|
||||
el.autoToggle.checked = state.auto;
|
||||
el.intervalSelect.value = String(state.interval);
|
||||
el.searchInput.value = state.search;
|
||||
el.logsAutoToggle.checked = logsState.auto;
|
||||
el.logsIntervalSelect.value = String(logsState.interval);
|
||||
el.logsWindowSelect.value = String(logsState.windowMs);
|
||||
el.logsLimitSelect.value = String(logsState.limit);
|
||||
el.logsSearchInput.value = logsState.search;
|
||||
applyTheme(appState.theme);
|
||||
persistPanel();
|
||||
persistEmailPreferences();
|
||||
persistLogPreferences();
|
||||
wire();
|
||||
renderWorkspace();
|
||||
renderAll();
|
||||
renderLogsAll();
|
||||
refreshMessages("initial");
|
||||
if (appState.panel === "logs") {
|
||||
ensureLogsReady();
|
||||
} else {
|
||||
refreshMessages("initial");
|
||||
}
|
||||
window.setInterval(() => {
|
||||
renderLiveClock();
|
||||
renderLogsLiveClock();
|
||||
@@ -1025,12 +1058,14 @@ function clientApp(config) {
|
||||
|
||||
el.autoToggle.addEventListener("change", () => {
|
||||
state.auto = el.autoToggle.checked;
|
||||
persistEmailPreferences();
|
||||
scheduleRefresh();
|
||||
renderStatus();
|
||||
});
|
||||
|
||||
el.intervalSelect.addEventListener("change", () => {
|
||||
state.interval = Number(el.intervalSelect.value) || config.defaultRefreshMs;
|
||||
persistEmailPreferences();
|
||||
scheduleRefresh();
|
||||
renderStatus();
|
||||
});
|
||||
@@ -1104,23 +1139,27 @@ function clientApp(config) {
|
||||
|
||||
el.logsAutoToggle.addEventListener("change", () => {
|
||||
logsState.auto = el.logsAutoToggle.checked;
|
||||
persistLogPreferences();
|
||||
scheduleLogsRefresh();
|
||||
renderLogsStatus();
|
||||
});
|
||||
|
||||
el.logsIntervalSelect.addEventListener("change", () => {
|
||||
logsState.interval = Number(el.logsIntervalSelect.value) || config.defaultRefreshMs;
|
||||
persistLogPreferences();
|
||||
scheduleLogsRefresh();
|
||||
renderLogsStatus();
|
||||
});
|
||||
|
||||
el.logsWindowSelect.addEventListener("change", () => {
|
||||
logsState.windowMs = Number(el.logsWindowSelect.value) || config.defaultLogWindowMs;
|
||||
persistLogPreferences();
|
||||
refreshLogs("window");
|
||||
});
|
||||
|
||||
el.logsLimitSelect.addEventListener("change", () => {
|
||||
logsState.limit = Number(el.logsLimitSelect.value) || config.defaultLogLimit;
|
||||
persistLogPreferences();
|
||||
refreshLogs("limit");
|
||||
});
|
||||
|
||||
@@ -1135,6 +1174,16 @@ function clientApp(config) {
|
||||
applyLogsFilter();
|
||||
});
|
||||
|
||||
el.logsExpandAllButton.addEventListener("click", () => {
|
||||
logsState.filtered.forEach((event) => logsState.openIds.add(event.id));
|
||||
renderLogsList();
|
||||
});
|
||||
|
||||
el.logsCollapseAllButton.addEventListener("click", () => {
|
||||
logsState.openIds.clear();
|
||||
renderLogsList();
|
||||
});
|
||||
|
||||
el.logsScrollToTopButton.addEventListener("click", () => {
|
||||
scrollPaneToTop(el.logsContentPane);
|
||||
});
|
||||
@@ -1146,12 +1195,14 @@ function clientApp(config) {
|
||||
el.logsGroupSelect.addEventListener("change", async () => {
|
||||
logsState.group = el.logsGroupSelect.value;
|
||||
logsState.stream = "";
|
||||
persistLogPreferences();
|
||||
await refreshLogStreams();
|
||||
await refreshLogs("group");
|
||||
});
|
||||
|
||||
el.logsStreamSelect.addEventListener("change", () => {
|
||||
logsState.stream = el.logsStreamSelect.value;
|
||||
persistLogPreferences();
|
||||
refreshLogs("stream");
|
||||
});
|
||||
|
||||
@@ -1162,6 +1213,9 @@ function clientApp(config) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const logEvent = getLogEvent(button.dataset.id);
|
||||
|
||||
if (!logEvent) {
|
||||
@@ -1228,10 +1282,13 @@ function clientApp(config) {
|
||||
}
|
||||
|
||||
appState.panel = panel;
|
||||
persistPanel();
|
||||
renderWorkspace();
|
||||
|
||||
if (panel === "logs") {
|
||||
await ensureLogsReady();
|
||||
} else if (!state.updatedAt && !state.loading) {
|
||||
await refreshMessages("panel");
|
||||
}
|
||||
|
||||
scheduleRefresh();
|
||||
@@ -1341,6 +1398,7 @@ function clientApp(config) {
|
||||
} catch (error) {
|
||||
logsState.error = error.message || "Unknown log group refresh error";
|
||||
} finally {
|
||||
persistLogPreferences();
|
||||
renderLogsAll();
|
||||
}
|
||||
}
|
||||
@@ -1376,6 +1434,7 @@ function clientApp(config) {
|
||||
logsState.stream = "";
|
||||
logsState.error = error.message || "Unknown log stream refresh error";
|
||||
} finally {
|
||||
persistLogPreferences();
|
||||
renderLogsAll();
|
||||
}
|
||||
}
|
||||
@@ -1451,6 +1510,7 @@ function clientApp(config) {
|
||||
logsState.filtered = !search
|
||||
? [...logsState.events]
|
||||
: logsState.events.filter((event) => logHaystack(event).includes(search));
|
||||
persistLogPreferences();
|
||||
renderLogsAll({ renderList: shouldRenderList });
|
||||
}
|
||||
|
||||
@@ -1482,6 +1542,7 @@ function clientApp(config) {
|
||||
state.filtered = !search
|
||||
? [...state.messages]
|
||||
: state.messages.filter((message) => haystack(message).includes(search));
|
||||
persistEmailPreferences();
|
||||
renderAll({ renderList: shouldRenderList });
|
||||
}
|
||||
|
||||
@@ -1791,31 +1852,29 @@ function clientApp(config) {
|
||||
}
|
||||
|
||||
el.logsEmpty.hidden = true;
|
||||
el.logsList.innerHTML = logsState.filtered.map((event, index) => renderLogEvent(event, index)).join("");
|
||||
el.logsList.innerHTML = logsState.filtered.map((event) => renderLogEvent(event)).join("");
|
||||
bindLogToggles();
|
||||
updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton);
|
||||
}
|
||||
|
||||
function renderLogEvent(event, index) {
|
||||
const open = logsState.openIds.has(event.id) || (!logsState.hasInteracted && index === 0) ? "open" : "";
|
||||
|
||||
function renderLogEvent(event) {
|
||||
return `
|
||||
<details class="logEvent" data-id="${escapeHtml(event.id)}" ${open}>
|
||||
<details class="logEvent" data-id="${escapeHtml(event.id)}" ${logsState.openIds.has(event.id) ? "open" : ""}>
|
||||
<summary class="logSummary">
|
||||
<div class="logSummaryTop">
|
||||
<div class="logMeta">
|
||||
<span class="time">${escapeHtml(formatDateTime(event.timestamp))}</span>
|
||||
<span class="tag logTag" title="${escapeHtml(event.logStreamName || "Unknown stream")}">${escapeHtml(event.logStreamName || "Unknown stream")}</span>
|
||||
</div>
|
||||
<span class="tag">${escapeHtml(formatRelative(event.timestamp || Date.now()))}</span>
|
||||
<div class="logSummaryActions">
|
||||
<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>
|
||||
<p class="logPreview">${escapeHtml(event.preview || "No preview available.")}</p>
|
||||
<p class="logPreview jsonSyntax">${renderLogPreviewContent(event)}</p>
|
||||
</summary>
|
||||
<div class="logBody">
|
||||
<div class="logCodeWrap">
|
||||
<button class="mini logCopyButton" type="button" data-log-action="copy" data-id="${escapeHtml(event.id)}">Copy JSON</button>
|
||||
<pre>${escapeHtml(formatLogMessage(event.message))}</pre>
|
||||
</div>
|
||||
<pre class="jsonSyntax">${renderLogBodyContent(event.message)}</pre>
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
@@ -1830,8 +1889,6 @@ function clientApp(config) {
|
||||
return;
|
||||
}
|
||||
|
||||
logsState.hasInteracted = true;
|
||||
|
||||
if (details.open) {
|
||||
logsState.openIds.add(id);
|
||||
} else {
|
||||
@@ -2065,14 +2122,17 @@ function clientApp(config) {
|
||||
el.logsStatusChip.textContent = message;
|
||||
}
|
||||
|
||||
function getInitialTheme() {
|
||||
try {
|
||||
const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
function getInitialPanel() {
|
||||
const storedPanel = readStoredValue(PANEL_STORAGE_KEY);
|
||||
return storedPanel === "logs" ? "logs" : "emails";
|
||||
}
|
||||
|
||||
if (storedTheme === "dark" || storedTheme === "light") {
|
||||
return storedTheme;
|
||||
}
|
||||
} catch {}
|
||||
function getInitialTheme() {
|
||||
const storedTheme = readStoredValue(THEME_STORAGE_KEY);
|
||||
|
||||
if (storedTheme === "dark" || storedTheme === "light") {
|
||||
return storedTheme;
|
||||
}
|
||||
|
||||
return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light";
|
||||
}
|
||||
@@ -2101,6 +2161,78 @@ function clientApp(config) {
|
||||
el.themeToggle.title = isDark ? "Switch to light theme" : "Switch to dark theme";
|
||||
}
|
||||
|
||||
function persistPanel() {
|
||||
writeStoredValue(PANEL_STORAGE_KEY, appState.panel);
|
||||
}
|
||||
|
||||
function persistEmailPreferences() {
|
||||
writeStoredJson(EMAIL_PREFERENCES_STORAGE_KEY, {
|
||||
search: state.search,
|
||||
auto: state.auto,
|
||||
interval: state.interval
|
||||
});
|
||||
}
|
||||
|
||||
function persistLogPreferences() {
|
||||
writeStoredJson(LOG_PREFERENCES_STORAGE_KEY, {
|
||||
group: logsState.group,
|
||||
stream: logsState.stream,
|
||||
search: logsState.search,
|
||||
auto: logsState.auto,
|
||||
interval: logsState.interval,
|
||||
windowMs: logsState.windowMs,
|
||||
limit: logsState.limit
|
||||
});
|
||||
}
|
||||
|
||||
function getStoredPreferences(key) {
|
||||
try {
|
||||
const rawValue = window.localStorage.getItem(key);
|
||||
|
||||
if (!rawValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedValue = JSON.parse(rawValue);
|
||||
return parsedValue && typeof parsedValue === "object" && !Array.isArray(parsedValue) ? parsedValue : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readStoredValue(key) {
|
||||
try {
|
||||
return window.localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredValue(key, value) {
|
||||
try {
|
||||
window.localStorage.setItem(key, String(value));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function writeStoredJson(key, value) {
|
||||
try {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getStoredText(value, fallback = "") {
|
||||
return typeof value === "string" ? value : fallback;
|
||||
}
|
||||
|
||||
function getStoredBoolean(value, fallback) {
|
||||
return typeof value === "boolean" ? value : fallback;
|
||||
}
|
||||
|
||||
function getStoredNumber(value, allowedValues, fallback) {
|
||||
const normalizedValue = Number(value);
|
||||
return Number.isFinite(normalizedValue) && allowedValues.includes(normalizedValue) ? normalizedValue : fallback;
|
||||
}
|
||||
|
||||
function scrollPaneToTop(element) {
|
||||
if (!element) {
|
||||
return;
|
||||
@@ -2181,6 +2313,74 @@ function clientApp(config) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderLogPreviewContent(event) {
|
||||
const parsedLog = tryParseLogJson(event?.message);
|
||||
|
||||
if (!parsedLog.ok) {
|
||||
return escapeHtml(event?.preview || "No preview available.");
|
||||
}
|
||||
|
||||
const compactJson = JSON.stringify(parsedLog.value);
|
||||
const previewText = compactJson.length > 220 ? `${compactJson.slice(0, 217)}...` : compactJson;
|
||||
return highlightJsonText(previewText);
|
||||
}
|
||||
|
||||
function renderLogBodyContent(message) {
|
||||
const parsedLog = tryParseLogJson(message);
|
||||
|
||||
if (!parsedLog.ok) {
|
||||
return escapeHtml(formatLogMessage(message));
|
||||
}
|
||||
|
||||
return highlightJsonText(JSON.stringify(parsedLog.value, null, 2));
|
||||
}
|
||||
|
||||
function tryParseLogJson(message) {
|
||||
const value = String(message || "").trim();
|
||||
|
||||
if (!value) {
|
||||
return { ok: false, value: null };
|
||||
}
|
||||
|
||||
try {
|
||||
return { ok: true, value: JSON.parse(value) };
|
||||
} catch {
|
||||
return { ok: false, value: null };
|
||||
}
|
||||
}
|
||||
|
||||
function highlightJsonText(value) {
|
||||
const source = String(value ?? "");
|
||||
const tokenRegex =
|
||||
/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?)/g;
|
||||
let html = "";
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const match of source.matchAll(tokenRegex)) {
|
||||
const [token] = match;
|
||||
const index = match.index ?? 0;
|
||||
let className = "jsonNumber";
|
||||
|
||||
html += escapeHtml(source.slice(lastIndex, index));
|
||||
|
||||
if (token.endsWith(":")) {
|
||||
className = "jsonKey";
|
||||
} else if (token === "true" || token === "false") {
|
||||
className = "jsonBoolean";
|
||||
} else if (token === "null") {
|
||||
className = "jsonNull";
|
||||
} else if (token.startsWith('"')) {
|
||||
className = "jsonString";
|
||||
}
|
||||
|
||||
html += `<span class="${className}">${escapeHtml(token)}</span>`;
|
||||
lastIndex = index + token.length;
|
||||
}
|
||||
|
||||
html += escapeHtml(source.slice(lastIndex));
|
||||
return html;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
|
||||
Reference in New Issue
Block a user