Files
pocketbase/ui/src/logs/logsList.js
T
2026-05-24 09:30:30 +03:00

472 lines
19 KiB
JavaScript

import { logLevel } from "./logLevel";
const perPage = 50;
export function logsList(logsSettings) {
const data = store({
logs: [],
lastLoadCount: 0,
lastPage: 1,
bulkSelected: {},
get canLoadMore() {
return data.lastLoadCount >= perPage;
},
get totalSelected() {
return Object.keys(data.bulkSelected).length;
},
get areAllSelected() {
return data.logs.length && data.logs.length == data.totalSelected;
},
});
// used as loose guard to prevent new logs to constantly push the old ones to later pages
let loadStartDate;
async function load(reset = false) {
logsSettings.isListLoading = true;
try {
let page;
if (reset) {
page = 1;
loadStartDate = new Date().toISOString().replace("T", " ");
} else {
page = data.lastPage + 1;
}
const normalizedFilter = (logsSettings.presets || []).concat(
app.utils.normalizeSearchFilter(logsSettings.filter, ["level", "message", "data"]),
);
if (logsSettings.zoom?.min && logsSettings.zoom?.max) {
const minDate = new Date(logsSettings.zoom.min * 1000);
minDate.setSeconds(0);
minDate.setMilliseconds(0);
const min = app.utils.toRFC3339Datetime(minDate);
const maxDate = new Date(logsSettings.zoom.max * 1000);
maxDate.setSeconds(59);
maxDate.setMilliseconds(999);
const max = app.utils.toRFC3339Datetime(maxDate);
normalizedFilter.push(`created >= "${min}" && created <= "${max}"`);
} else {
normalizedFilter.push(`created <= "${loadStartDate}"`);
}
const result = await app.pb.logs.getList(page, perPage, {
skipTotal: 1,
sort: "-@rowid",
requestKey: "logs_list",
filter: normalizedFilter
.filter(Boolean)
.map((f) => "(" + f + ")")
.join("&&"),
});
if (result.page == 1) {
data.logs = [];
data.bulkSelected = {};
}
data.lastPage = result.page;
data.lastLoadCount = result.items.length;
for (let i = 0; i < result.items.length; i++) {
app.utils.pushOrReplaceObject(data.logs, result.items[i]);
// yield to main (with room to "breathe")
if (i > 1 && i % 20 == 0) {
await new Promise((r) => setTimeout(r, 20));
}
}
logsSettings.isListLoading = false;
if (!logsSettings.isFirstLoadReady) {
logsSettings.isFirstLoadReady = true;
}
} catch (err) {
if (!err.isAbort) {
logsSettings.isListLoading = false;
app.checkApiError(err);
}
}
}
// note: allways assign a new object to trigger the getter's Object.keys
function selectAll(state = true) {
const selected = {};
if (state) {
for (let log of data.logs) {
selected[log.id] = log;
}
}
data.bulkSelected = selected;
}
function getLogPreviewKeys(log) {
let keys = [];
if (!log.data) {
return keys;
}
if (log.data.type == "request") {
const requestKeys = ["status", "execTime", "auth", "authId", "userIP"];
for (let key of requestKeys) {
if (typeof log.data[key] != "undefined") {
keys.push({ key });
}
}
// add the referer if it is from a different source
if (log.data.referer && !log.data.referer.includes(window.location.host)) {
keys.push({ key: "referer" });
}
} else {
// extract the first 6 keys (excluding the error and details)
const allKeys = Object.keys(log.data);
for (const key of allKeys) {
if (key != "error" && key != "details" && keys.length < 6) {
keys.push({ key });
}
}
}
// ensure that the error and detail keys are last
if (log.data.error) {
keys.push({ key: "error", label: "danger" });
}
if (log.data.details) {
keys.push({ key: "details", label: "warning" });
}
return keys;
}
const dateFilenameRegex = /[-:\. ]/gi;
function downloadSelected() {
// extract the bulk selected log objects sorted desc
const selected = Object.values(data.bulkSelected).sort((a, b) => {
if (a.created < b.created) {
return 1;
}
if (a.created > b.created) {
return -1;
}
return 0;
});
if (!selected.length) {
return; // nothing to download
}
console.log(data.bulkSelected);
if (selected.length == 1) {
return app.utils.downloadJSON(
selected[0],
"log_" + selected[0].created.replaceAll(dateFilenameRegex, "") + ".json",
);
}
const to = selected[0].created.replaceAll(dateFilenameRegex, "");
const from = selected[selected.length - 1].created.replaceAll(dateFilenameRegex, "");
return app.utils.downloadJSON(selected, `${selected.length}_logs_${from}_to_${to}.json`);
}
const watchers = [];
return t.div(
{
pbEvent: "logsList",
className: "page-table-wrapper",
onmount(el) {
watchers.push(
// always reset zoom on filter or preset change
watch(
() => [logsSettings.filter, logsSettings.presets?.length],
() => {
logsSettings.zoom = {};
},
),
watch(
() => [
logsSettings.reset,
logsSettings.filter,
logsSettings.presets?.length,
logsSettings.zoom?.min,
logsSettings.zoom?.max,
],
() => {
load(true);
if (el) {
el.scrollTop = 0;
}
},
),
);
},
onunmount() {
watchers.forEach((w) => w?.unwatch());
},
},
t.table(
{ className: () => `logs-table ${data.logs?.length > perPage ? "optimize" : ""}` },
t.thead(
null,
t.tr(
null,
t.th(
{ className: "col-bulk-select" },
t.div(
{
className: "field",
hidden: () => logsSettings.isLoading,
},
t.input({
id: "logs_select_all",
type: "checkbox",
checked: () => data.areAllSelected,
onchange: (e) => selectAll(e.target.checked),
}),
t.label({ htmlFor: "logs_select_all" }),
),
t.span({
className: "loader",
hidden: () => !logsSettings.isLoading,
}),
),
t.th(
{ className: "col-field-name-level" },
t.div(
{ className: "inline-flex gap-5" },
t.i({ className: "ri-bookmark-line", ariaHidden: true }),
t.span({ textContent: "Level" }),
),
),
t.th(
{ className: "col-field-name-message" },
t.div(
{ className: "inline-flex gap-5" },
t.i({ className: "ri-file-list-2-line", ariaHidden: true }),
t.span({ textContent: "Message" }),
),
),
t.th(
{ className: "col-field-type-date col-field-name-created" },
t.div(
{ className: "inline-flex gap-5" },
t.i({ className: "ri-calendar-line", ariaHidden: true }),
t.span({ textContent: "Created" }),
),
),
t.th({ className: "col-meta" }),
),
),
t.tbody(
null,
() => {
if (!data.logs?.length) {
return t.tr(
null,
t.td(
{ colSpan: 99 },
() => {
if (logsSettings.isListLoading) {
return t.span({ className: "skeleton-loader" });
}
return t.div(
{ className: "sticky-content txt-center txt-hint" },
t.div({ className: "txt-bold" }, "No logs found."),
t.button(
{
hidden: () =>
logsSettings.filter?.length || app.utils.isEmpty(logsSettings.zoom),
type: "button",
className: "btn secondary expanded-lg m-t-10",
onclick() {
logsSettings.zoom = {};
},
},
t.span({ className: "txt" }, "Reset zoom"),
),
t.button(
{
hidden: () => !logsSettings.filter?.length,
type: "button",
className: "btn secondary expanded-lg m-t-10",
onclick() {
logsSettings.filter = "";
},
},
t.span({ className: "txt" }, "Clear search"),
),
);
},
),
);
}
return data.logs.map((log) => {
return t.tr(
{
rid: log.id,
tabIndex: 0,
role: "button",
className: () => `handle ${log.data.type == "request" ? "log-request" : ""}`,
onclick: () => {
logsSettings.activeLogIdOrModel = log;
},
onkeypress: (e) => {
if (e.key == "Enter" || e.key == " ") {
e.preventDefault();
logsSettings.activeLogIdOrModel = log;
}
},
},
() => {
return [
t.td(
{
className: "col-bulk-select",
onclick: (e) => e.stopPropagation(),
onkeypress: (e) => e.stopPropagation(),
},
t.div(
{ className: "field" },
t.input({
id: "cb_" + log.id,
type: "checkbox",
checked: () => !!data.bulkSelected[log.id],
onchange: (e) => {
const bulkSelected = JSON.parse(
JSON.stringify(data.bulkSelected),
);
if (e.target.checked) {
bulkSelected[log.id] = log;
} else {
delete bulkSelected[log.id];
}
// reassign to trigger the getter's Object.keys
data.bulkSelected = bulkSelected;
},
}),
t.label({ htmlFor: "cb_" + log.id }),
),
),
t.td({ className: "col-field-name-level" }, logLevel(log)),
t.td(
{ className: "col-field-name-message" },
t.div(
{ className: "content-primary" },
() => app.utils.truncate(log.message, 1000),
),
t.div(
{
className: "content-secondary",
},
() => {
const labels = [];
const keyItems = getLogPreviewKeys(log);
for (const keyItem of keyItems) {
let value;
if (app.utils.logDataFormatters[keyItem.key]) {
value = app.utils.logDataFormatters[keyItem.key](log);
} else {
value = app.utils.displayValue(log.data[keyItem.key], 80);
}
labels.push(
t.span(
{ className: `label sm ${keyItem.label || ""}` },
`${keyItem.key}: ${value}`,
),
);
}
return labels;
},
),
),
t.td(
{
className: "col-field-type-date col-field-name-created",
},
app.components.formattedDate({
value: () => log.created,
short: false,
}),
),
t.td(
{ className: "col-meta" },
t.i({ className: "ri-arrow-right-line", ariaHidden: true }),
),
];
},
);
});
},
t.tr(
{ hidden: () => !data.canLoadMore },
t.td(
{ colSpan: 99 },
t.button(
{
className: () =>
`btn lg secondary load-more-btn ${
logsSettings.isListLoading ? "transparent loading" : ""
}`,
disabled: () => logsSettings.isListLoading,
onclick: () => load(),
},
t.span({ className: "txt" }, "Load older"),
),
),
),
),
),
t.div(
{ className: "bulkbar-wrapper" },
t.div(
{
hidden: () => data.totalSelected == 0,
className: "bulkbar logs-bulkbar",
},
t.span(
{ className: "txt" },
"Selected ",
t.strong(null, () => data.totalSelected),
() => ` ${data.totalSelected == 1 ? "log" : "logs"}`,
),
t.button(
{
type: "button",
className: "btn sm secondary pill m-r-auto",
onclick: () => selectAll(false),
},
t.span({ className: "txt" }, "Reset"),
),
t.button(
{
type: "button",
className: "btn sm pill",
onclick: () => downloadSelected(),
},
t.i({ className: "ri-download-line", ariaHidden: true }),
t.span({ className: "txt" }, "JSON"),
),
),
),
);
}