initial public commit
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
<script>
|
||||
import { Request } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import FormattedDate from "@/components/base/FormattedDate.svelte";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
|
||||
let logPanel;
|
||||
let item = new Request();
|
||||
|
||||
export function show(model) {
|
||||
item = model;
|
||||
|
||||
return logPanel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return logPanel?.hide();
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel bind:this={logPanel} class="overlay-panel-lg log-panel" on:hide on:show>
|
||||
<svelte:fragment slot="header">
|
||||
<h4>Request log</h4>
|
||||
</svelte:fragment>
|
||||
|
||||
<table class="table-compact table-border">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">ID</td>
|
||||
<td>{item.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">Status</td>
|
||||
<td>
|
||||
<span class="label" class:label-danger={item.status >= 400}>
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">Method</td>
|
||||
<td>{item.method?.toUpperCase()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">Auth</td>
|
||||
<td>{item.auth}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">URL</td>
|
||||
<td>{item.url}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">Referer</td>
|
||||
<td>
|
||||
{#if item.referer}
|
||||
<a href={item.referer} target="_blank" rel="noopener noreferrer">
|
||||
{item.referer}
|
||||
</a>
|
||||
{:else}
|
||||
<span class="txt-hint">N/A</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">IP</td>
|
||||
<td>{item.ip}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">UserAgent</td>
|
||||
<td>{item.userAgent}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">Meta</td>
|
||||
<td>
|
||||
{#if !CommonHelper.isEmpty(item.meta)}
|
||||
<CodeBlock content={JSON.stringify(item.meta, null, 2)} />
|
||||
{:else}
|
||||
<span class="txt-hint">N/A</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">Created</td>
|
||||
<td><FormattedDate date={item.created} /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-secondary" on:click={() => hide()}>
|
||||
<span class="txt">Close</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
@@ -0,0 +1,180 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { scale } from "svelte/transition";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import {
|
||||
Chart,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LineController,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Filler,
|
||||
Tooltip,
|
||||
} from "chart.js";
|
||||
import "chartjs-adapter-luxon";
|
||||
|
||||
export let filter = "";
|
||||
export let presets = "";
|
||||
|
||||
let chartCanvas;
|
||||
let chartInst;
|
||||
let chartData = [];
|
||||
let totalRequests = 0;
|
||||
let isLoading = false;
|
||||
|
||||
$: if (typeof filter !== "undefined" || typeof presets !== "undefined") {
|
||||
load();
|
||||
}
|
||||
|
||||
$: if (typeof chartData !== "undefined" && chartInst) {
|
||||
chartInst.data.datasets[0].data = chartData;
|
||||
chartInst.update();
|
||||
}
|
||||
|
||||
export async function load() {
|
||||
isLoading = true;
|
||||
|
||||
return ApiClient.Logs.getRequestsStats({
|
||||
filter: [presets, filter].filter(Boolean).join("&&"),
|
||||
})
|
||||
.then((result) => {
|
||||
resetData();
|
||||
for (let item of result) {
|
||||
chartData.push({
|
||||
x: CommonHelper.getDateTime(item.date).toLocal().toJSDate(),
|
||||
y: item.total,
|
||||
});
|
||||
totalRequests += item.total;
|
||||
}
|
||||
|
||||
// add current time marker to the chart
|
||||
chartData.push({
|
||||
x: new Date(),
|
||||
y: undefined,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err !== null) {
|
||||
resetData();
|
||||
console.warn(err);
|
||||
ApiClient.errorResponseHandler(err, false);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function resetData() {
|
||||
totalRequests = 0;
|
||||
chartData = [];
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
Chart.register(LineElement, PointElement, LineController, LinearScale, TimeScale, Filler, Tooltip);
|
||||
|
||||
chartInst = new Chart(chartCanvas, {
|
||||
type: "line",
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: "Total requests",
|
||||
data: chartData,
|
||||
borderColor: "#ef4565",
|
||||
pointBackgroundColor: "#ef4565",
|
||||
backgroundColor: "rgb(239,69,101,0.05)",
|
||||
borderWidth: 2,
|
||||
pointBorderWidth: 0,
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
animation: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: "index",
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: "#edf0f3",
|
||||
borderColor: "#dee3e8",
|
||||
},
|
||||
ticks: {
|
||||
precision: 0,
|
||||
maxTicksLimit: 6,
|
||||
autoSkip: true,
|
||||
color: "#666f75",
|
||||
},
|
||||
},
|
||||
x: {
|
||||
type: "time",
|
||||
time: {
|
||||
unit: "hour",
|
||||
tooltipFormat: "DD h a",
|
||||
},
|
||||
grid: {
|
||||
borderColor: "#dee3e8",
|
||||
color: (c) => (c.tick.major ? "#edf0f3" : ""),
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: 15,
|
||||
autoSkip: true,
|
||||
maxRotation: 0,
|
||||
major: {
|
||||
enabled: true,
|
||||
},
|
||||
color: (c) => (c.tick.major ? "#16161a" : "#666f75"),
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return () => chartInst?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="chart-wrapper" class:loading={isLoading}>
|
||||
{#if isLoading}
|
||||
<div class="chart-loader loader" transition:scale={{ duration: 150 }} />
|
||||
{/if}
|
||||
<canvas bind:this={chartCanvas} class="chart-canvas" style="height: 250px; width: 100%;" />
|
||||
</div>
|
||||
|
||||
<div class="txt-hint m-t-xs txt-right">
|
||||
{#if isLoading}
|
||||
Loading...
|
||||
{:else}
|
||||
{totalRequests}
|
||||
{totalRequests === 1 ? "log" : "logs"}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.chart-wrapper.loading .chart-canvas {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.chart-loader {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,201 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import SortHeader from "@/components/base/SortHeader.svelte";
|
||||
import FormattedDate from "@/components/base/FormattedDate.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const labelMethodClass = {
|
||||
get: "label-info",
|
||||
post: "label-success",
|
||||
patch: "label-warning",
|
||||
delete: "label-danger",
|
||||
};
|
||||
|
||||
export let filter = "";
|
||||
export let presets = "";
|
||||
export let sort = "-rowid";
|
||||
|
||||
let items = [];
|
||||
let currentPage = 1;
|
||||
let totalItems = 0;
|
||||
let isLoading = false;
|
||||
|
||||
$: if (typeof sort !== "undefined" || typeof filter !== "undefined" || typeof presets !== "undefined") {
|
||||
clearList();
|
||||
load(1);
|
||||
}
|
||||
|
||||
$: canLoadMore = totalItems > items.length;
|
||||
|
||||
export async function load(page = 1) {
|
||||
isLoading = true;
|
||||
|
||||
return ApiClient.Logs.getRequestsList(page, 40, {
|
||||
sort: sort,
|
||||
filter: [presets, filter].filter(Boolean).join("&&"),
|
||||
})
|
||||
.then((result) => {
|
||||
if (page <= 1) {
|
||||
clearList();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
items = items.concat(result.items);
|
||||
currentPage = result.page;
|
||||
totalItems = result.totalItems;
|
||||
|
||||
dispatch("load", items);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err !== null) {
|
||||
isLoading = false;
|
||||
console.warn(err);
|
||||
clearList();
|
||||
ApiClient.errorResponseHandler(err, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearList() {
|
||||
items = [];
|
||||
currentPage = 1;
|
||||
totalItems = 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table" class:table-loading={isLoading}>
|
||||
<thead>
|
||||
<tr>
|
||||
<SortHeader disable class="col-field-method" name="method" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class="ri-global-line" />
|
||||
<span class="txt">method</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
|
||||
<SortHeader disable class="col-type-text col-field-url" name="url" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("url")} />
|
||||
<span class="txt">url</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
|
||||
<SortHeader disable class="col-type-text col-field-referer" name="referer" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("url")} />
|
||||
<span class="txt">referer</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
|
||||
<SortHeader disable class="col-type-number col-field-status" name="status" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("number")} />
|
||||
<span class="txt">status</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
|
||||
<SortHeader disable class="col-type-date col-field-created" name="created" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("date")} />
|
||||
<span class="txt">created</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
<th class="col-type-action min-width" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each items as item (item.id)}
|
||||
<tr
|
||||
tabindex="0"
|
||||
class="row-handle"
|
||||
on:click={() => dispatch("select", item)}
|
||||
on:keydown={(e) => {
|
||||
if (e.code === "Enter") {
|
||||
e.preventDefault();
|
||||
dispatch("select", item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td class="col-type-text col-field-method min-width">
|
||||
<span class="label txt-uppercase {labelMethodClass[item.method.toLowerCase()]}">
|
||||
{item.method?.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="col-type-text col-field-url">
|
||||
<span class="txt txt-ellipsis" title={item.url}>
|
||||
{item.url}
|
||||
</span>
|
||||
{#if item.meta?.errorMessage || item.meta?.errorData}
|
||||
<i class="ri-error-warning-line txt-danger m-l-5 m-r-5" title="Error" />
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<td class="col-type-text col-field-referer">
|
||||
<span class="txt txt-ellipsis" class:txt-hint={!item.referer} title={item.referer}>
|
||||
{item.referer || "N/A"}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="col-type-number col-field-status">
|
||||
<span class="label" class:label-danger={item.status >= 400}>
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="col-type-date col-field-created">
|
||||
<FormattedDate date={item.created} />
|
||||
</td>
|
||||
|
||||
<td class="col-type-action min-width">
|
||||
<i class="ri-arrow-right-line" />
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#if isLoading}
|
||||
<tr>
|
||||
<td colspan="99" class="p-xs">
|
||||
<span class="skeleton-loader" />
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="99" class="txt-center txt-hint p-xs">
|
||||
<h6>No logs found.</h6>
|
||||
{#if filter?.length}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-hint btn-expanded m-t-sm"
|
||||
on:click={() => (filter = "")}
|
||||
>
|
||||
<span class="txt">Clear filters</span>
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{#if items.length}
|
||||
<small class="block txt-hint txt-right m-t-sm">Showing {items.length} of {totalItems}</small>
|
||||
{/if}
|
||||
|
||||
{#if items.length && canLoadMore}
|
||||
<div class="block txt-center m-t-xs">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-lg btn-secondary btn-expanded"
|
||||
class:btn-loading={isLoading}
|
||||
class:btn-disabled={isLoading}
|
||||
on:click={() => load(currentPage + 1)}
|
||||
>
|
||||
<span class="txt">Load more ({totalItems - items.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,75 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Searchbar from "@/components/base/Searchbar.svelte";
|
||||
import LogsList from "@/components/logs/LogsList.svelte";
|
||||
import LogsChart from "@/components/logs/LogsChart.svelte";
|
||||
import LogViewPanel from "@/components/logs/LogViewPanel.svelte";
|
||||
|
||||
const ADMIN_LOGS_LOCAL_STORAGE_KEY = "includeAdminLogs";
|
||||
|
||||
let logPanel;
|
||||
let filter = "";
|
||||
let includeAdminLogs = window.localStorage?.getItem(ADMIN_LOGS_LOCAL_STORAGE_KEY) << 0;
|
||||
let refreshToken = 1;
|
||||
|
||||
$: presets = !includeAdminLogs ? 'auth!="admin"' : "";
|
||||
|
||||
$: if (typeof includeAdminLogs !== "undefined" && window.localStorage) {
|
||||
window.localStorage.setItem(ADMIN_LOGS_LOCAL_STORAGE_KEY, includeAdminLogs << 0);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
refreshToken++;
|
||||
}
|
||||
|
||||
CommonHelper.setDocumentTitle("Request logs");
|
||||
</script>
|
||||
|
||||
<main class="page-wrapper">
|
||||
<div class="page-header-wrapper m-b-0">
|
||||
<header class="page-header">
|
||||
<nav class="breadcrumbs">
|
||||
<div class="breadcrumb-item">Request logs</div>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-circle btn-secondary"
|
||||
use:tooltip={{ text: "Refresh", position: "right" }}
|
||||
on:click={refresh}
|
||||
>
|
||||
<i class="ri-refresh-line" />
|
||||
</button>
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
<div class="inline-flex">
|
||||
<Field class="form-field form-field-toggle m-0" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={includeAdminLogs} />
|
||||
<label for={uniqueId}>Include requests by admins</label>
|
||||
</Field>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Searchbar
|
||||
value={filter}
|
||||
placeholder="Search logs, ex. status > 200"
|
||||
extraAutocompleteKeys={["method", "url", "ip", "referer", "status", "auth", "userAgent"]}
|
||||
on:submit={(e) => (filter = e.detail)}
|
||||
/>
|
||||
|
||||
<div class="clearfix m-b-xs" />
|
||||
|
||||
{#key refreshToken}
|
||||
<LogsChart bind:filter {presets} />
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
{#key refreshToken}
|
||||
<LogsList bind:filter {presets} on:select={(e) => logPanel?.show(e?.detail)} />
|
||||
{/key}
|
||||
</main>
|
||||
|
||||
<LogViewPanel bind:this={logPanel} />
|
||||
Reference in New Issue
Block a user