Convert admin panel to svelte

This commit is contained in:
2021-05-25 22:15:29 +02:00
parent c0045fa9d0
commit 74defb0cf0
18 changed files with 492 additions and 267 deletions

View File

@@ -16,6 +16,7 @@
"svelte": "^3.0.0"
},
"dependencies": {
"chart.js": "^2.8.0",
"sirv-cli": "^1.0.0"
}
}

View File

@@ -34,9 +34,7 @@ export default [
"modal",
"user_buckets",
"user_file_manager",
"admin_abuse_reporters",
"admin_abuse_reports",
"admin_ip_bans",
"admin_panel",
].map((name, index) => ({
input: `src/${name}.js`,
output: {

View File

@@ -1,8 +0,0 @@
import App from './admin_abuse_reporters/AbuseReporters.svelte';
const app = new App({
target: document.getElementById("page_content"),
props: {}
});
export default app;

View File

@@ -1,8 +0,0 @@
import App from './admin_abuse_reports/AbuseReports.svelte';
const app = new App({
target: document.getElementById("page_content"),
props: {}
});
export default app;

View File

@@ -1,4 +1,4 @@
import App from './admin_ip_bans/IPBans.svelte';
import App from './admin_panel/AdminPanel.svelte';
const app = new App({
target: document.getElementById("page_content"),

View File

@@ -91,9 +91,6 @@ onMount(get_reporters);
<div class="limit_width">
<div class="toolbar" style="text-align: left;">
<a class="button" href="/admin">
<i class="icon">arrow_back</i> Return to admin panel
</a>
<div class="toolbar_spacer"></div>
<button class:button_highlight={creating} on:click={() => {creating = !creating}}>
<i class="icon">create</i> Add abuse reporter
@@ -171,6 +168,7 @@ onMount(get_reporters);
left: 10px;
height: 100px;
width: 100px;
z-index: 1000;
}
.toolbar {
display: flex;

View File

@@ -100,15 +100,12 @@ onMount(() => {
<div class="limit_width">
<div class="toolbar" style="text-align: left;">
<a class="button" href="/admin">
<i class="icon">arrow_back</i> Return to admin panel
</a>
<div class="toolbar_spacer"></div>
<div>
Start: <input type="date" bind:this={startPicker}/>
End: <input type="date" bind:this={endPicker}/>
</div>
<button on:click={get_reports}><i class="icon">refresh</i></button>
<div>Start:</div>
<input type="date" bind:this={startPicker}/>
<div>End:</div>
<input type="date" bind:this={endPicker}/>
<button on:click={get_reports}>Go</button>
</div>
<h2>Pending</h2>
@@ -131,7 +128,7 @@ onMount(() => {
.spinner_container {
position: absolute;
top: 10px;
right: 10px;
left: 10px;
height: 100px;
width: 100px;
z-index: 1000;
@@ -140,6 +137,7 @@ onMount(() => {
display: flex;
flex-direction: row;
width: 100%;
align-items: center;
}
.toolbar > * { flex: 0 0 auto; }
.toolbar_spacer { flex: 1 1 auto; }

View File

@@ -0,0 +1,75 @@
<script>
import AbuseReporters from "./AbuseReporters.svelte"
import AbuseReports from "./AbuseReports.svelte"
import IpBans from "./IPBans.svelte"
import Home from "./Home.svelte"
import { onMount } from "svelte";
let page = ""
let navigate = (path, title) => {
page = path
window.document.title = title+" ~ pixeldrain"
window.history.pushState(
{}, window.document.title, "/admin/"+path
)
}
onMount(() => {
let newpage = window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1)
if (newpage === "admin") {
newpage = ""
}
page = newpage
})
</script>
<div>
<a class="button"
href="/admin"
class:button_highlight={page === ""}
on:click|preventDefault={() => {navigate("", "Status")}}>
<i class="icon">home</i>
Status
</a>
<a class="button" href="/admin/abuse">
<i class="icon">block</i>
Block files
</a>
<a class="button"
href="/admin/abuse_reports"
class:button_highlight={page === "abuse_reports"}
on:click|preventDefault={() => {navigate("abuse_reports", "Abuse reports")}}>
<i class="icon">flag</i>
User abuse reports
</a>
<a class="button"
href="/admin/abuse_reporters"
class:button_highlight={page === "abuse_reporters"}
on:click|preventDefault={() => {navigate("abuse_reporters", "Abuse reporters")}}>
<i class="icon">report</i>
E-mail abuse reporters
</a>
<a class="button"
href="/admin/ip_bans"
class:button_highlight={page === "ip_bans"}
on:click|preventDefault={() => {navigate("ip_bans", "IP bans")}}>
<i class="icon">remove_circle</i>
IP bans
</a>
<a class="button" href="/admin/globals">
<i class="icon">edit</i>
Update global settings
</a>
<hr/>
{#if page === ""}
<Home></Home>
{:else if page === "abuse_reports"}
<AbuseReports></AbuseReports>
{:else if page === "abuse_reporters"}
<AbuseReporters></AbuseReporters>
{:else if page === "ip_bans"}
<IpBans></IpBans>
{/if}
</div>

View File

@@ -0,0 +1,272 @@
<script>
import { onDestroy, onMount } from "svelte";
import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "../util/Formatting.svelte";
import Chart from "../util/Chart.svelte";
let graphViews
let graphBandwidth
let graphTimeout = null
let start_time = ""
let end_time = ""
let total_bandwidth = 0
let total_views = 0
const loadGraph = (minutes, interval, live) => {
if (graphTimeout !== null) { clearTimeout(graphTimeout) }
if (live) {
graphTimeout = setTimeout(() => { loadGraph(minutes, interval, true) }, 10000)
}
let today = new Date()
let start = new Date()
start.setMinutes(start.getMinutes() - minutes)
fetch(
window.api_endpoint + "/admin/files/timeseries" +
"?start=" + start.toISOString() +
"&end=" + today.toISOString() +
"&interval=" + interval
).then(resp => {
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
return resp.json();
}).then(resp => {
resp.views.timestamps.forEach((val, idx) => {
let date = new Date(val);
let dateStr = ("00" + (date.getMonth() + 1)).slice(-2);
dateStr += "-" + ("00" + date.getDate()).slice(-2);
dateStr += " " + ("00" + date.getHours()).slice(-2);
dateStr += ":" + ("00" + date.getMinutes()).slice(-2);
resp.views.timestamps[idx] = " " + dateStr + " "; // Poor man's padding
});
graphViews.chart().data.labels = resp.views.timestamps;
graphViews.chart().data.datasets[0].data = resp.views.amounts;
graphBandwidth.chart().data.labels = resp.views.timestamps;
graphBandwidth.chart().data.datasets[0].data = resp.bandwidth.amounts;
graphViews.update()
graphBandwidth.update()
start_time = resp.views.timestamps[0]
end_time = resp.views.timestamps.slice(-1)[0];
total_bandwidth = resp.bandwidth.amounts.reduce((acc, val) => acc + val)
total_views = resp.views.amounts.reduce((acc, val) => acc + val)
})
}
// Load performance statistics
let lastOrder;
let status = {
db_latency: 0,
db_time: "",
local_read_size: 0,
local_read_size_per_sec: 0,
local_reads: 0,
local_reads_per_sec: 0,
peers: [],
query_statistics: [],
remote_read_size: 0,
remote_read_size_per_sec: 0,
remote_reads: 0,
remote_reads_per_sec: 0,
running_since: "",
stats_watcher_listeners: 0,
stats_watcher_threads: 0
}
function getStats(order) {
lastOrder = order
fetch(window.api_endpoint + "/status").then(
resp => resp.json()
).then(resp => {
// Sort all queries by the selected sort column
resp.query_statistics.sort((a, b) => {
if (typeof (a[order]) === "number") {
// Sort ints from high to low
return b[order] - a[order]
} else {
// Sort strings alphabetically
return a[order].localeCompare(b[order])
}
})
// Sort individual query callers by frequency
resp.query_statistics.forEach((v) => {
v.callers.sort((a, b) => b.count - a.count)
})
status = resp
})
}
let statsInterval = null
onMount(() => {
loadGraph(10080, 10, false);
getStats("calls")
statsInterval = setInterval(() => {
getStats(lastOrder)
}, 10000)
})
onDestroy(() => {
if (graphTimeout !== null) {
clearTimeout(graphTimeout)
}
if (statsInterval !== null) {
clearInterval(statsInterval)
}
})
</script>
<div>
<div class="limit_width">
<h3>Bandwidth and views</h3>
</div>
<div class="highlight_dark" style="margin-bottom: 6px;">
<button on:click={() => { loadGraph(1440, 1, true) }}>Day</button>
<button on:click={() => { loadGraph(10080, 10, false) }}>Week</button>
<button on:click={() => { loadGraph(20160, 60, false) }}>Two Weeks</button>
<button on:click={() => { loadGraph(43200, 60, false) }}>Month</button>
<button on:click={() => { loadGraph(131400, 1440, false) }}>Quarter</button>
<button on:click={() => { loadGraph(262800, 1440, false) }}>Half-year</button>
<button on:click={() => { loadGraph(525600, 1440, false) }}>Year</button>
<button on:click={() => { loadGraph(1051200, 1440, false) }}>Two Years</button>
</div>
<Chart bind:this={graphBandwidth} dataType="bytes" label="Bandwidth" />
<hr/>
<Chart bind:this={graphViews} dataType="number" label="Views" />
<div class="highlight_dark">
Total usage from {start_time} to {end_time}<br/>
{formatDataVolume(total_bandwidth, 3)} bandwidth and {formatThousands(total_views, 3)} views
</div>
<br/>
<a class="button" href="/api/admin/call_stack">Call stack</a>
<a class="button" href="/api/admin/heap_profile">Heap profile</a>
<a class="button" href="/api/admin/cpu_profile">CPU profile (wait 1 min)</a>
<br/>
<div class="limit_width">
<table>
<tr>
<td>DB Time</td>
<td>{formatDate(new Date(status.db_time), true, true, true)}</td>
<td>DB Latency</td>
<td>{formatNumber(status.db_latency / 1000, 3)} ms</td>
</tr>
</table>
<h3>Pixelstore peers</h3>
<table>
<thead>
<tr>
<td>Address</td>
<td>Pos</td>
<td>Alive</td>
<td>Err</td>
<td>1m</td>
<td>5m</td>
<td>15m</td>
<td>Ping</td>
<td>Free</td>
<td>Min free</td>
</tr>
</thead>
<tbody>
{#each status.peers as peer}
<tr class="peer_row"
class:highlight_red={peer.free_space < peer.min_free_space / 2 || !peer.reachable}
class:highlight_blue={peer.free_space < peer.min_free_space}
class:highlight_green={peer.reachable}
>
<td>{peer.address}</td>
<td>{peer.position}</td>
<td>{peer.reachable}</td>
<td>{peer.unreachable_count}</td>
<td>{peer.load_1_min}</td>
<td>{peer.load_5_min}</td>
<td>{peer.load_15_min}</td>
<td>{formatDuration(peer.latency)}</td>
<td>{formatDataVolume(peer.free_space, 3)}</td>
<td>{formatDataVolume(peer.min_free_space, 3)}</td>
</tr>
{/each}
</tbody>
</table>
<h3>Pixelstore stats</h3>
<table>
<thead>
<tr>
<td>Local reads</td>
<td>Local read size</td>
<td>Remote reads</td>
<td>Remote read size</td>
</tr>
</thead>
<tbody>
<tr>
<td>{status.local_reads}</td>
<td>{formatDataVolume(status.local_read_size, 4)}</td>
<td>{status.remote_reads}</td>
<td>{formatDataVolume(status.remote_read_size, 4)}</td>
</tr>
<tr>
<td>{status.local_reads_per_sec.toPrecision(4)} / s</td>
<td>{formatDataVolume(status.local_read_size_per_sec, 4)} / s</td>
<td>{status.remote_reads_per_sec.toPrecision(4)} / s</td>
<td>{formatDataVolume(status.remote_read_size_per_sec, 4)} /s</td>
</tr>
</tbody>
</table>
<h3>Websocket statistics</h3>
<table>
<thead>
<tr>
<td>Watcher</td>
<td>Threads</td>
<td>Listeners</td>
<td>Avg</td>
</tr>
</thead>
<tbody>
<tr>
<td>File statistics</td>
<td>{status.stats_watcher_threads}</td>
<td>{status.stats_watcher_listeners}</td>
<td>{(status.stats_watcher_listeners / status.stats_watcher_threads).toPrecision(3)}</td>
</tr>
</tbody>
</table>
<h3>Query statistics</h3>
<table>
<thead>
<tr>
<td style="cursor: pointer;" on:click={() => { getStats('query_name') }}>Query</td>
<td style="cursor: pointer;" on:click={() => { getStats('calls') }}>Calls</td>
<td style="cursor: pointer;" on:click={() => { getStats('average_duration') }}>Average Duration</td>
<td style="cursor: pointer;" on:click={() => { getStats('total_duration') }}>Total Duration</td>
<td>Callers</td>
</tr>
</thead>
<tbody id="tstat_body">
{#each status.query_statistics as q}
<tr>
<td>{q.query_name}</td>
<td>{q.calls}</td>
<td>{formatDuration(q.average_duration)}</td>
<td>{formatDuration(q.total_duration)}</td>
<td>
{#each q.callers as caller}
{caller.count}x {caller.name}<br/>
{/each}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
<style>
.peer_row {
text-align: left;
}
</style>

View File

@@ -86,9 +86,6 @@ onMount(get_bans);
<div class="limit_width">
<div class="toolbar" style="text-align: left;">
<a class="button" href="/admin">
<i class="icon">arrow_back</i> Return to admin panel
</a>
<div class="toolbar_spacer"></div>
<button class:button_highlight={creating} on:click={() => {creating = !creating}}>
<i class="icon">create</i> Add IP ban
@@ -145,14 +142,16 @@ onMount(get_bans);
<td>Reason</td>
<td>Ban time</td>
<td>Expire time</td>
<td>Offences</td>
<td></td>
</tr>
{#each rows as row}
<tr>
<td>{row.address}</td>
<td>{rows.reason}</td>
<td>{row.reason}</td>
<td>{formatDate(row.ban_time, true, true, false)}</td>
<td>{formatDate(row.expire_time, true, true, false)}</td>
<td>{row.offences}</td>
<td>
<button on:click|preventDefault={() => {delete_ban(row.address)}} class="button button_red">
<i class="icon">delete</i>
@@ -170,6 +169,7 @@ onMount(get_bans);
left: 10px;
height: 100px;
width: 100px;
z-index: 1000;
}
.toolbar {
display: flex;

View File

@@ -0,0 +1,96 @@
<script>
import { onMount } from "svelte";
import { formatDataVolume, formatNumber } from "./Formatting.svelte";
import { Chart } from "chart.js"
let chart_element
let chart_object
export let label = "label"
export let dataType = ""
export const chart = () => {
return chart_object
}
export const update = () => {
return chart_object.update()
}
Chart.defaults.global.defaultFontColor = "#b3b3b3";
Chart.defaults.global.defaultFontSize = 15;
Chart.defaults.global.defaultFontFamily = "system-ui, sans-serif";
Chart.defaults.global.maintainAspectRatio = false;
Chart.defaults.global.elements.point.radius = 0;
Chart.defaults.global.tooltips.mode = "index";
Chart.defaults.global.tooltips.axis = "x";
Chart.defaults.global.tooltips.intersect = false;
Chart.defaults.global.animation.duration = 500;
Chart.defaults.global.animation.easing = "linear";
onMount(() => {
chart_object = new Chart(
chart_element.getContext("2d"),
{
type: 'line',
data: {
datasets: [
{
label: label,
backgroundColor: window.highlight_color,
borderWidth: 0,
lineTension: 0,
fill: true,
yAxisID: "ax_1"
}
]
},
options: {
legend: { display: false },
scales: {
yAxes: [
{
type: "linear",
display: true,
position: "left",
id: "ax_1",
ticks: {
callback: function (value, index, values) {
if (dataType == "bytes") {
return formatDataVolume(value, 3);
}
return formatNumber(value, 3);
},
beginAtZero: true
},
gridLines: { display: true },
}
],
xAxes: [
{
ticks: {
sampleSize: 1,
padding: 4,
minRotation: 0,
maxRotation: 0
},
gridLines: { display: false }
}
]
}
}
}
);
})
</script>
<div class="chart-container">
<canvas bind:this={chart_element}></canvas>
</div>
<style>
.chart-container {
position: relative;
width: 100%;
height: 200px;
}
</style>