Frontend node logs loading indicator

This commit is contained in:
ditatompel 2024-05-07 20:55:57 +07:00
parent d04473a807
commit 59da1cb7eb
No known key found for this signature in database
GPG key ID: 31D3D06D77950979
4 changed files with 179 additions and 146 deletions

31
frontend/src/app.d.ts vendored
View file

@ -1,16 +1,27 @@
// See https://kit.svelte.dev/docs/types#app // See https://kit.svelte.dev/docs/types#app
// for information about these interfaces // for information about these interfaces
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
// interface Locals {} // interface Locals {}
// interface PageData {} // interface PageData {}
// interface PageState {} // interface PageState {}
// interface Platform {} // interface Platform {}
} }
interface ImportMetaEnv { interface ImportMetaEnv {
VITE_API_URL: string; VITE_API_URL: string;
} }
interface MoneroNode {
id: number;
hostname: string;
ip: string;
port: number;
protocol: string;
is_tor: boolean;
is_available: boolean;
nettype: string;
}
} }
export {}; export {};

View file

@ -127,7 +127,7 @@
<MainNav /> <MainNav />
<div class="pt-10 md:pt-12"> <div class="pt-10 md:pt-12 min-h-screen">
<slot /> <slot />
</div> </div>

View file

@ -1,7 +1,7 @@
<script> <script>
import { DataHandler } from '@vincjo/datatables/remote'; import { DataHandler } from '@vincjo/datatables/remote';
import { format, formatDistance } from 'date-fns'; import { format, formatDistance } from 'date-fns';
import { loadData, formatBytes } from './api-handler'; import { loadData, loadNodeInfo, formatBytes } from './api-handler';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { import {
DtSrRowsPerPage, DtSrRowsPerPage,
@ -45,6 +45,9 @@
let filterProberId = 0; let filterProberId = 0;
let filterStatus = -1; let filterStatus = -1;
/** @type {MoneroNode | null} */
let nodeInfo;
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 }); const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
let rows = handler.getRows(); let rows = handler.getRows();
@ -93,6 +96,9 @@
}); });
onMount(() => { onMount(() => {
pageId = new URLSearchParams(window.location.search).get('node_id') || '0'; pageId = new URLSearchParams(window.location.search).get('node_id') || '0';
loadNodeInfo(pageId).then((data) => {
nodeInfo = data;
});
handler.filter(pageId, 'node_id'); handler.filter(pageId, 'node_id');
handler.onChange((state) => loadData(state)); handler.onChange((state) => loadData(state));
handler.invalidate(); handler.invalidate();
@ -109,146 +115,156 @@
</div> </div>
<div class="section-container text-center"> <div class="section-container text-center">
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1> <h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
<p class="mx-auto max-w-3xl">
<strong>Monero remote node</strong> is a device on the internet running the Monero software with
full copy of the Monero blockchain that doesn't run on the same local machine where the Monero
wallet is located.
</p>
</div> </div>
<div class="mx-auto w-full max-w-3xl px-20"> <div class="mx-auto w-full max-w-3xl px-20">
<hr class="!border-primary-400-500-token !border-t-4 !border-double" /> <hr class="!border-primary-400-500-token !border-t-4 !border-double" />
</div> </div>
</header> </header>
<section id="introduction "> {#if nodeInfo === undefined}
<div class="section-container text-center !max-w-4xl"> <div class="section-container mx-auto w-full max-w-3xl text-center">
<p> <p>Loading...</p>
Remote node can be used by people who, for their own reasons (usually because of hardware
requirements, disk space, or technical abilities), cannot/don't want to run their own node and
prefer to relay on one publicly available on the Monero network.
</p>
<p>
Using an open node will allow to make a transaction instantaneously, without the need to
download the blockchain and sync to the Monero network first, but at the cost of the control
over your privacy. the <strong>Monero community suggests to always run your own node</strong> to
obtain the maximum possible privacy and to help decentralize the network.
</p>
</div> </div>
</section> {:else if nodeInfo === null}
<div class="section-container mx-auto w-full max-w-3xl text-center">
<section id="monero-remote-node"> <p>Node ID does not exist</p>
</div>
{:else}
<div class="section-container"> <div class="section-container">
<div class="space-y-2 overflow-x-auto"> <div class="table-container mx-auto w-full max-w-3xl">
<div class="flex justify-between"> <table class="table">
<DtSrRowsPerPage {handler} />
<div class="invisible flex place-items-center md:visible">
<label for="autoRefreshInterval">Auto Refresh:</label>
<select
class="select ml-2"
id="autoRefreshInterval"
bind:value={intervalValue}
on:change={startInterval}
>
{#each intervalOptions as { value, label }}
<option {value}>{label}</option>
{/each}
</select>
</div>
<div class="flex place-items-center">
<button
id="reloadDt"
name="reloadDt"
class="variant-filled-primary btn"
on:click={reloadData}>Reload</button
>
</div>
</div>
<table class="table table-hover table-compact w-full table-auto">
<thead>
<tr>
<th>#ID</th>
<th><label for="prober_id">Prober</label></th>
<th><label for="status">Status</label></th>
<th>Height</th>
<th>Adjusted Time</th>
<th>DB Size</th>
<th>Difficulty</th>
<DtSrThSort {handler} orderBy="estimate_fee">Est. Fee</DtSrThSort>
<DtSrThSort {handler} orderBy="date_checked">Date Checked</DtSrThSort>
<DtSrThSort {handler} orderBy="fetch_runtime">Runtime</DtSrThSort>
</tr>
<tr>
<th colspan="2">
<select
id="prober_id"
name="prober_id"
class="select variant-form-material"
bind:value={filterProberId}
on:change={() => {
handler.filter(filterProberId, 'prober_id');
handler.invalidate();
}}
>
<option value={0}>Any</option>
</select>
</th>
<th colspan="2">
<select
id="status"
name="status"
class="select variant-form-material"
bind:value={filterStatus}
on:change={() => {
handler.filter(filterStatus, 'status');
handler.invalidate();
}}
>
<option value={-1}>Any</option>
<option value="1">Online</option>
<option value="0">Offline</option>
</select>
</th>
<DtSrThFilter
{handler}
filterBy="failed_reason"
placeholder="Filter reason"
colspan={6}
/>
</tr>
</thead>
<tbody> <tbody>
{#each $rows as row (row.id)} <tr>
<tr> <td class="font-bold">Hostname:Port</td>
<td>{row.id}</td> <td>{nodeInfo?.hostname}:{nodeInfo?.port}</td>
<td>{row.prober_id}</td> </tr>
<td>{row.status === 1 ? 'OK' : 'ERR'}</td> <tr>
{#if row.status !== 1} <td class="font-bold">Public IP</td>
<td colspan="5">{row.failed_reason ?? ''}</td> <td>{nodeInfo?.ip}</td>
{:else} </tr>
<td class="text-right">{row.height.toLocaleString(undefined)}</td> <tr>
<td>{format(row.adjusted_time * 1000, 'yyyy-MM-dd HH:mm')}</td> <td class="font-bold">Net Type</td>
<td class="text-right">{formatBytes(row.database_size, 2)}</td> <td>{nodeInfo?.nettype.toUpperCase()}</td>
<td class="text-right">{formatHashes(row.difficulty)}</td> </tr></tbody
<td class="text-right">{row.estimate_fee.toLocaleString(undefined)}</td> >
{/if}
<td>
{format(row.date_checked * 1000, 'PP HH:mm')}<br />
{formatDistance(row.date_checked * 1000, new Date(), { addSuffix: true })}
</td>
<td class="text-right">{parseRuntime(row.fetch_runtime)}</td>
</tr>
{/each}
</tbody>
</table> </table>
<div class="flex justify-between mb-2">
<DtSrRowCount {handler} />
<DtSrPagination {handler} />
</div>
</div> </div>
</div> </div>
</section>
<section id="node-logs">
<div class="section-container">
<div class="space-y-2 overflow-x-auto">
<div class="flex justify-between">
<DtSrRowsPerPage {handler} />
<div class="invisible flex place-items-center md:visible">
<label for="autoRefreshInterval">Auto Refresh:</label>
<select
class="select ml-2"
id="autoRefreshInterval"
bind:value={intervalValue}
on:change={startInterval}
>
{#each intervalOptions as { value, label }}
<option {value}>{label}</option>
{/each}
</select>
</div>
<div class="flex place-items-center">
<button
id="reloadDt"
name="reloadDt"
class="variant-filled-primary btn"
on:click={reloadData}>Reload</button
>
</div>
</div>
<table class="table table-hover table-compact w-full table-auto">
<thead>
<tr>
<th>#ID</th>
<th><label for="prober_id">Prober</label></th>
<th><label for="status">Status</label></th>
<th>Height</th>
<th>Adjusted Time</th>
<th>DB Size</th>
<th>Difficulty</th>
<DtSrThSort {handler} orderBy="estimate_fee">Est. Fee</DtSrThSort>
<DtSrThSort {handler} orderBy="date_checked">Date Checked</DtSrThSort>
<DtSrThSort {handler} orderBy="fetch_runtime">Runtime</DtSrThSort>
</tr>
<tr>
<th colspan="2">
<select
id="prober_id"
name="prober_id"
class="select variant-form-material"
bind:value={filterProberId}
on:change={() => {
handler.filter(filterProberId, 'prober_id');
handler.invalidate();
}}
>
<option value={0}>Any</option>
</select>
</th>
<th colspan="2">
<select
id="status"
name="status"
class="select variant-form-material"
bind:value={filterStatus}
on:change={() => {
handler.filter(filterStatus, 'status');
handler.invalidate();
}}
>
<option value={-1}>Any</option>
<option value="1">Online</option>
<option value="0">Offline</option>
</select>
</th>
<DtSrThFilter
{handler}
filterBy="failed_reason"
placeholder="Filter reason"
colspan={6}
/>
</tr>
</thead>
<tbody>
{#each $rows as row (row.id)}
<tr>
<td>{row.id}</td>
<td>{row.prober_id}</td>
<td>{row.status === 1 ? 'OK' : 'ERR'}</td>
{#if row.status !== 1}
<td colspan="5">{row.failed_reason ?? ''}</td>
{:else}
<td class="text-right">{row.height.toLocaleString(undefined)}</td>
<td>{format(row.adjusted_time * 1000, 'yyyy-MM-dd HH:mm')}</td>
<td class="text-right">{formatBytes(row.database_size, 2)}</td>
<td class="text-right">{formatHashes(row.difficulty)}</td>
<td class="text-right">{row.estimate_fee.toLocaleString(undefined)}</td>
{/if}
<td>
{format(row.date_checked * 1000, 'PP HH:mm')}<br />
{formatDistance(row.date_checked * 1000, new Date(), { addSuffix: true })}
</td>
<td class="text-right">{parseRuntime(row.fetch_runtime)}</td>
</tr>
{/each}
</tbody>
</table>
<div class="flex justify-between mb-2">
<DtSrRowCount {handler} />
<DtSrPagination {handler} />
</div>
</div>
</div>
</section>
{/if}
<style lang="postcss"> <style lang="postcss">
.section-container { .section-container {

View file

@ -8,21 +8,27 @@ export const loadData = async (state) => {
return json.data.items ?? []; return json.data.items ?? [];
}; };
export const loadNodeInfo = async (nodeId) => {
const response = await fetch(apiUri(`/api/v1/nodes/id/${nodeId}`));
const json = await response.json();
return json.data;
};
/** /**
* @param {number} bytes * @param {number} bytes
* @param {number} decimals * @param {number} decimals
* @returns {string} * @returns {string}
*/ */
export const formatBytes = (bytes, decimals = 2) => { export const formatBytes = (bytes, decimals = 2) => {
if (!+bytes) return '0 Bytes'; if (!+bytes) return '0 Bytes';
const k = 1024; const k = 1024;
const dm = decimals < 0 ? 0 : decimals; const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}; };
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => { const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {