feature/IO-3587-Commision-Cut - Improved localstack client
This commit is contained in:
@@ -689,6 +689,8 @@ function renderHtml() {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<input id="logsSearchInput" class="search" type="search" placeholder="Search stream name or log content..." autocomplete="off">
|
<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="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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -721,11 +723,11 @@ function renderStyles() {
|
|||||||
return `
|
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);}
|
: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}
|
*{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}
|
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,input,select,textarea{font:inherit}
|
||||||
button{cursor:pointer}
|
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}
|
.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)}
|
.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)}
|
.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{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}
|
||||||
.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}
|
.workspacePanel[hidden]{display:none}
|
||||||
.toolControls{display:grid;gap:8px}
|
.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}
|
.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}
|
.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}
|
.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}
|
.logSummary::-webkit-details-marker{display:none}
|
||||||
.logSummaryTop{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center}
|
.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}
|
.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}
|
.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}
|
.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)}
|
.logBody{padding:8px 12px 12px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)}
|
||||||
.logCodeWrap{position:relative}
|
.logCopyButton{box-shadow:none}
|
||||||
.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:12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff}
|
||||||
.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}
|
.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}
|
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}
|
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}
|
.placeholder,.inlineError{padding:12px}
|
||||||
@@ -865,6 +872,11 @@ function renderStyles() {
|
|||||||
body[data-theme="dark"] .head h2,
|
body[data-theme="dark"] .head h2,
|
||||||
body[data-theme="dark"] .stat strong,
|
body[data-theme="dark"] .stat strong,
|
||||||
body[data-theme="dark"] h1{color:#edf2f7}
|
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"] .meta,
|
||||||
body[data-theme="dark"] .helper,
|
body[data-theme="dark"] .helper,
|
||||||
body[data-theme="dark"] .lede,
|
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{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)}
|
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: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) {
|
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 = {
|
const appState = {
|
||||||
panel: "emails",
|
panel: getInitialPanel(),
|
||||||
logsReady: false,
|
logsReady: false,
|
||||||
theme: getInitialTheme()
|
theme: getInitialTheme()
|
||||||
};
|
};
|
||||||
const THEME_STORAGE_KEY = "localstack-inspector-theme";
|
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
messages: [],
|
messages: [],
|
||||||
filtered: [],
|
filtered: [],
|
||||||
search: "",
|
search: getStoredText(storedEmailPreferences?.search),
|
||||||
auto: true,
|
auto: getStoredBoolean(storedEmailPreferences?.auto, true),
|
||||||
interval: config.defaultRefreshMs,
|
interval: getStoredNumber(storedEmailPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs),
|
||||||
loading: false,
|
loading: false,
|
||||||
error: "",
|
error: "",
|
||||||
updatedAt: 0,
|
updatedAt: 0,
|
||||||
@@ -923,13 +944,13 @@ function clientApp(config) {
|
|||||||
streams: [],
|
streams: [],
|
||||||
events: [],
|
events: [],
|
||||||
filtered: [],
|
filtered: [],
|
||||||
group: config.defaultLogGroup || "",
|
group: getStoredText(storedLogPreferences?.group, config.defaultLogGroup || ""),
|
||||||
stream: "",
|
stream: getStoredText(storedLogPreferences?.stream),
|
||||||
search: "",
|
search: getStoredText(storedLogPreferences?.search),
|
||||||
auto: true,
|
auto: getStoredBoolean(storedLogPreferences?.auto, true),
|
||||||
interval: config.defaultRefreshMs,
|
interval: getStoredNumber(storedLogPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs),
|
||||||
windowMs: config.defaultLogWindowMs,
|
windowMs: getStoredNumber(storedLogPreferences?.windowMs, LOG_WINDOWS, config.defaultLogWindowMs),
|
||||||
limit: config.defaultLogLimit,
|
limit: getStoredNumber(storedLogPreferences?.limit, LOG_LIMITS, config.defaultLogLimit),
|
||||||
loading: false,
|
loading: false,
|
||||||
error: "",
|
error: "",
|
||||||
updatedAt: 0,
|
updatedAt: 0,
|
||||||
@@ -938,7 +959,6 @@ function clientApp(config) {
|
|||||||
newest: 0,
|
newest: 0,
|
||||||
searchedLogStreams: 0,
|
searchedLogStreams: 0,
|
||||||
openIds: new Set(),
|
openIds: new Set(),
|
||||||
hasInteracted: false,
|
|
||||||
listSignature: ""
|
listSignature: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -976,6 +996,8 @@ function clientApp(config) {
|
|||||||
logsLimitSelect: document.getElementById("logsLimitSelect"),
|
logsLimitSelect: document.getElementById("logsLimitSelect"),
|
||||||
logsSearchInput: document.getElementById("logsSearchInput"),
|
logsSearchInput: document.getElementById("logsSearchInput"),
|
||||||
logsClearSearchButton: document.getElementById("logsClearSearchButton"),
|
logsClearSearchButton: document.getElementById("logsClearSearchButton"),
|
||||||
|
logsExpandAllButton: document.getElementById("logsExpandAllButton"),
|
||||||
|
logsCollapseAllButton: document.getElementById("logsCollapseAllButton"),
|
||||||
logsScrollToTopButton: document.getElementById("logsScrollToTopButton"),
|
logsScrollToTopButton: document.getElementById("logsScrollToTopButton"),
|
||||||
logsStatusChip: document.getElementById("logsStatusChip"),
|
logsStatusChip: document.getElementById("logsStatusChip"),
|
||||||
logsTotalStat: document.getElementById("logsTotalStat"),
|
logsTotalStat: document.getElementById("logsTotalStat"),
|
||||||
@@ -991,16 +1013,27 @@ function clientApp(config) {
|
|||||||
logsContentPane: document.getElementById("logsContentPane")
|
logsContentPane: document.getElementById("logsContentPane")
|
||||||
};
|
};
|
||||||
|
|
||||||
el.intervalSelect.value = String(config.defaultRefreshMs);
|
el.autoToggle.checked = state.auto;
|
||||||
el.logsIntervalSelect.value = String(config.defaultRefreshMs);
|
el.intervalSelect.value = String(state.interval);
|
||||||
el.logsWindowSelect.value = String(config.defaultLogWindowMs);
|
el.searchInput.value = state.search;
|
||||||
el.logsLimitSelect.value = String(config.defaultLogLimit);
|
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);
|
applyTheme(appState.theme);
|
||||||
|
persistPanel();
|
||||||
|
persistEmailPreferences();
|
||||||
|
persistLogPreferences();
|
||||||
wire();
|
wire();
|
||||||
renderWorkspace();
|
renderWorkspace();
|
||||||
renderAll();
|
renderAll();
|
||||||
renderLogsAll();
|
renderLogsAll();
|
||||||
refreshMessages("initial");
|
if (appState.panel === "logs") {
|
||||||
|
ensureLogsReady();
|
||||||
|
} else {
|
||||||
|
refreshMessages("initial");
|
||||||
|
}
|
||||||
window.setInterval(() => {
|
window.setInterval(() => {
|
||||||
renderLiveClock();
|
renderLiveClock();
|
||||||
renderLogsLiveClock();
|
renderLogsLiveClock();
|
||||||
@@ -1025,12 +1058,14 @@ function clientApp(config) {
|
|||||||
|
|
||||||
el.autoToggle.addEventListener("change", () => {
|
el.autoToggle.addEventListener("change", () => {
|
||||||
state.auto = el.autoToggle.checked;
|
state.auto = el.autoToggle.checked;
|
||||||
|
persistEmailPreferences();
|
||||||
scheduleRefresh();
|
scheduleRefresh();
|
||||||
renderStatus();
|
renderStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.intervalSelect.addEventListener("change", () => {
|
el.intervalSelect.addEventListener("change", () => {
|
||||||
state.interval = Number(el.intervalSelect.value) || config.defaultRefreshMs;
|
state.interval = Number(el.intervalSelect.value) || config.defaultRefreshMs;
|
||||||
|
persistEmailPreferences();
|
||||||
scheduleRefresh();
|
scheduleRefresh();
|
||||||
renderStatus();
|
renderStatus();
|
||||||
});
|
});
|
||||||
@@ -1104,23 +1139,27 @@ function clientApp(config) {
|
|||||||
|
|
||||||
el.logsAutoToggle.addEventListener("change", () => {
|
el.logsAutoToggle.addEventListener("change", () => {
|
||||||
logsState.auto = el.logsAutoToggle.checked;
|
logsState.auto = el.logsAutoToggle.checked;
|
||||||
|
persistLogPreferences();
|
||||||
scheduleLogsRefresh();
|
scheduleLogsRefresh();
|
||||||
renderLogsStatus();
|
renderLogsStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.logsIntervalSelect.addEventListener("change", () => {
|
el.logsIntervalSelect.addEventListener("change", () => {
|
||||||
logsState.interval = Number(el.logsIntervalSelect.value) || config.defaultRefreshMs;
|
logsState.interval = Number(el.logsIntervalSelect.value) || config.defaultRefreshMs;
|
||||||
|
persistLogPreferences();
|
||||||
scheduleLogsRefresh();
|
scheduleLogsRefresh();
|
||||||
renderLogsStatus();
|
renderLogsStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.logsWindowSelect.addEventListener("change", () => {
|
el.logsWindowSelect.addEventListener("change", () => {
|
||||||
logsState.windowMs = Number(el.logsWindowSelect.value) || config.defaultLogWindowMs;
|
logsState.windowMs = Number(el.logsWindowSelect.value) || config.defaultLogWindowMs;
|
||||||
|
persistLogPreferences();
|
||||||
refreshLogs("window");
|
refreshLogs("window");
|
||||||
});
|
});
|
||||||
|
|
||||||
el.logsLimitSelect.addEventListener("change", () => {
|
el.logsLimitSelect.addEventListener("change", () => {
|
||||||
logsState.limit = Number(el.logsLimitSelect.value) || config.defaultLogLimit;
|
logsState.limit = Number(el.logsLimitSelect.value) || config.defaultLogLimit;
|
||||||
|
persistLogPreferences();
|
||||||
refreshLogs("limit");
|
refreshLogs("limit");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1135,6 +1174,16 @@ function clientApp(config) {
|
|||||||
applyLogsFilter();
|
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", () => {
|
el.logsScrollToTopButton.addEventListener("click", () => {
|
||||||
scrollPaneToTop(el.logsContentPane);
|
scrollPaneToTop(el.logsContentPane);
|
||||||
});
|
});
|
||||||
@@ -1146,12 +1195,14 @@ function clientApp(config) {
|
|||||||
el.logsGroupSelect.addEventListener("change", async () => {
|
el.logsGroupSelect.addEventListener("change", async () => {
|
||||||
logsState.group = el.logsGroupSelect.value;
|
logsState.group = el.logsGroupSelect.value;
|
||||||
logsState.stream = "";
|
logsState.stream = "";
|
||||||
|
persistLogPreferences();
|
||||||
await refreshLogStreams();
|
await refreshLogStreams();
|
||||||
await refreshLogs("group");
|
await refreshLogs("group");
|
||||||
});
|
});
|
||||||
|
|
||||||
el.logsStreamSelect.addEventListener("change", () => {
|
el.logsStreamSelect.addEventListener("change", () => {
|
||||||
logsState.stream = el.logsStreamSelect.value;
|
logsState.stream = el.logsStreamSelect.value;
|
||||||
|
persistLogPreferences();
|
||||||
refreshLogs("stream");
|
refreshLogs("stream");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1162,6 +1213,9 @@ function clientApp(config) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
const logEvent = getLogEvent(button.dataset.id);
|
const logEvent = getLogEvent(button.dataset.id);
|
||||||
|
|
||||||
if (!logEvent) {
|
if (!logEvent) {
|
||||||
@@ -1228,10 +1282,13 @@ function clientApp(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
appState.panel = panel;
|
appState.panel = panel;
|
||||||
|
persistPanel();
|
||||||
renderWorkspace();
|
renderWorkspace();
|
||||||
|
|
||||||
if (panel === "logs") {
|
if (panel === "logs") {
|
||||||
await ensureLogsReady();
|
await ensureLogsReady();
|
||||||
|
} else if (!state.updatedAt && !state.loading) {
|
||||||
|
await refreshMessages("panel");
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleRefresh();
|
scheduleRefresh();
|
||||||
@@ -1341,6 +1398,7 @@ function clientApp(config) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logsState.error = error.message || "Unknown log group refresh error";
|
logsState.error = error.message || "Unknown log group refresh error";
|
||||||
} finally {
|
} finally {
|
||||||
|
persistLogPreferences();
|
||||||
renderLogsAll();
|
renderLogsAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1376,6 +1434,7 @@ function clientApp(config) {
|
|||||||
logsState.stream = "";
|
logsState.stream = "";
|
||||||
logsState.error = error.message || "Unknown log stream refresh error";
|
logsState.error = error.message || "Unknown log stream refresh error";
|
||||||
} finally {
|
} finally {
|
||||||
|
persistLogPreferences();
|
||||||
renderLogsAll();
|
renderLogsAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1451,6 +1510,7 @@ function clientApp(config) {
|
|||||||
logsState.filtered = !search
|
logsState.filtered = !search
|
||||||
? [...logsState.events]
|
? [...logsState.events]
|
||||||
: logsState.events.filter((event) => logHaystack(event).includes(search));
|
: logsState.events.filter((event) => logHaystack(event).includes(search));
|
||||||
|
persistLogPreferences();
|
||||||
renderLogsAll({ renderList: shouldRenderList });
|
renderLogsAll({ renderList: shouldRenderList });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1482,6 +1542,7 @@ function clientApp(config) {
|
|||||||
state.filtered = !search
|
state.filtered = !search
|
||||||
? [...state.messages]
|
? [...state.messages]
|
||||||
: state.messages.filter((message) => haystack(message).includes(search));
|
: state.messages.filter((message) => haystack(message).includes(search));
|
||||||
|
persistEmailPreferences();
|
||||||
renderAll({ renderList: shouldRenderList });
|
renderAll({ renderList: shouldRenderList });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1791,31 +1852,29 @@ function clientApp(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
el.logsEmpty.hidden = true;
|
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();
|
bindLogToggles();
|
||||||
updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton);
|
updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLogEvent(event, index) {
|
function renderLogEvent(event) {
|
||||||
const open = logsState.openIds.has(event.id) || (!logsState.hasInteracted && index === 0) ? "open" : "";
|
|
||||||
|
|
||||||
return `
|
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">
|
<summary class="logSummary">
|
||||||
<div class="logSummaryTop">
|
<div class="logSummaryTop">
|
||||||
<div class="logMeta">
|
<div class="logMeta">
|
||||||
<span class="time">${escapeHtml(formatDateTime(event.timestamp))}</span>
|
<span class="time">${escapeHtml(formatDateTime(event.timestamp))}</span>
|
||||||
<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>
|
||||||
<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>
|
</div>
|
||||||
<p class="logPreview">${escapeHtml(event.preview || "No preview available.")}</p>
|
<p class="logPreview jsonSyntax">${renderLogPreviewContent(event)}</p>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="logBody">
|
<div class="logBody">
|
||||||
<div class="logCodeWrap">
|
<pre class="jsonSyntax">${renderLogBodyContent(event.message)}</pre>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
`;
|
`;
|
||||||
@@ -1830,8 +1889,6 @@ function clientApp(config) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logsState.hasInteracted = true;
|
|
||||||
|
|
||||||
if (details.open) {
|
if (details.open) {
|
||||||
logsState.openIds.add(id);
|
logsState.openIds.add(id);
|
||||||
} else {
|
} else {
|
||||||
@@ -2065,14 +2122,17 @@ function clientApp(config) {
|
|||||||
el.logsStatusChip.textContent = message;
|
el.logsStatusChip.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialTheme() {
|
function getInitialPanel() {
|
||||||
try {
|
const storedPanel = readStoredValue(PANEL_STORAGE_KEY);
|
||||||
const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
|
return storedPanel === "logs" ? "logs" : "emails";
|
||||||
|
}
|
||||||
|
|
||||||
if (storedTheme === "dark" || storedTheme === "light") {
|
function getInitialTheme() {
|
||||||
return storedTheme;
|
const storedTheme = readStoredValue(THEME_STORAGE_KEY);
|
||||||
}
|
|
||||||
} catch {}
|
if (storedTheme === "dark" || storedTheme === "light") {
|
||||||
|
return storedTheme;
|
||||||
|
}
|
||||||
|
|
||||||
return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light";
|
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";
|
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) {
|
function scrollPaneToTop(element) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
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) {
|
function escapeHtml(value) {
|
||||||
return String(value ?? "")
|
return String(value ?? "")
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
|
|||||||
Reference in New Issue
Block a user