472 lines
19 KiB
JavaScript
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"),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|