mirror of
https://github.com/ditatompel/xmr-remote-nodes.git
synced 2024-12-23 12:09:47 +00:00
Frontend node logs loading indicator
This commit is contained in:
parent
d04473a807
commit
59da1cb7eb
4 changed files with 179 additions and 146 deletions
31
frontend/src/app.d.ts
vendored
31
frontend/src/app.d.ts
vendored
|
@ -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 {};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
Loading…
Reference in a new issue