mirror of
https://github.com/ditatompel/xmr-remote-nodes.git
synced 2025-01-08 05:52:10 +07:00
parent
d5f510ae32
commit
d13a427c6d
18 changed files with 25 additions and 533 deletions
|
@ -1,21 +0,0 @@
|
|||
<script>
|
||||
import '../../app.css';
|
||||
import { Drawer } from '@skeletonlabs/skeleton';
|
||||
import { MainNav, MobileDrawer } from '$lib/components/navigation';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
</script>
|
||||
|
||||
<Drawer>
|
||||
<h2 class="p-4">Navigation</h2>
|
||||
<hr />
|
||||
<MobileDrawer />
|
||||
<hr />
|
||||
</Drawer>
|
||||
|
||||
<MainNav />
|
||||
|
||||
<div class="pt-10 md:pt-12 min-h-screen">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<Footer />
|
|
@ -1,21 +0,0 @@
|
|||
<script>
|
||||
import { Drawer } from '@skeletonlabs/skeleton';
|
||||
import { AdminNav, AdminSidebar, AdminMobileDrawer } from '$lib/components/navigation';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</svelte:head>
|
||||
<Drawer>
|
||||
<h2 class="p-4">Navigation</h2>
|
||||
<hr />
|
||||
<AdminMobileDrawer />
|
||||
<hr />
|
||||
</Drawer>
|
||||
|
||||
<AdminNav />
|
||||
<AdminSidebar />
|
||||
|
||||
<div class="min-h-screen bg-gray-100/80 p-4 pt-14 dark:bg-gray-900/80 sm:ml-64">
|
||||
<slot />
|
||||
</div>
|
|
@ -1,126 +0,0 @@
|
|||
<script>
|
||||
import { DataHandler } from '@vincjo/datatables/remote';
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
import { loadData } from './api-handler';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
DtSrThSort,
|
||||
DtSrThFilter,
|
||||
DtSrRowCount,
|
||||
DtSrAutoRefresh
|
||||
} from '$lib/components/datatables/server';
|
||||
|
||||
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
|
||||
let rows = handler.getRows();
|
||||
|
||||
/** @type {string | number} */
|
||||
let filterState = -1;
|
||||
/** @type {string | number} */
|
||||
let filterEnabled = -1;
|
||||
|
||||
onMount(() => {
|
||||
handler.onChange((state) => loadData(state));
|
||||
handler.invalidate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-4">
|
||||
<h1 class="h2 font-extrabold dark:text-white">Crons</h1>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<div class="flex justify-between">
|
||||
<div class="invisible flex place-items-center md:visible">
|
||||
<DtSrAutoRefresh {handler} />
|
||||
</div>
|
||||
<div class="flex place-items-center">
|
||||
<button
|
||||
id="reloadDt"
|
||||
name="reloadDt"
|
||||
class="variant-filled-primary btn"
|
||||
on:click={() => handler.invalidate()}>Reload</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-2 overflow-x-auto">
|
||||
<table class="table table-hover table-compact w-full table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<DtSrThSort {handler} orderBy="id">ID</DtSrThSort>
|
||||
<th>Title</th>
|
||||
<th>Slug</th>
|
||||
<th>Description</th>
|
||||
<DtSrThSort {handler} orderBy="run_every">Run Every</DtSrThSort>
|
||||
<DtSrThSort {handler} orderBy="last_run">Last Run</DtSrThSort>
|
||||
<DtSrThSort {handler} orderBy="next_run">Next Run</DtSrThSort>
|
||||
<DtSrThSort {handler} orderBy="run_time">Run Time</DtSrThSort>
|
||||
<th>State</th>
|
||||
<th>Enabled</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<DtSrThFilter {handler} filterBy="title" placeholder="Title" colspan={3} />
|
||||
<DtSrThFilter {handler} filterBy="description" placeholder="Description" colspan={5} />
|
||||
<th>
|
||||
<select
|
||||
id="fState"
|
||||
name="fState"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterState}
|
||||
on:change={() => {
|
||||
handler.filter(filterState, 'cron_state');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value={-1}>Any</option>
|
||||
<option value="1">Running</option>
|
||||
<option value="0">Idle</option>
|
||||
</select>
|
||||
</th>
|
||||
<th>
|
||||
<select
|
||||
id="fEnabled"
|
||||
name="fEnabled"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterEnabled}
|
||||
on:change={() => {
|
||||
handler.filter(filterEnabled, 'is_enabled');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value={-1}>Any</option>
|
||||
<option value="1">Yes</option>
|
||||
<option value="0">No</option>
|
||||
</select>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $rows as row (row.id)}
|
||||
<tr>
|
||||
<td>{row.id}</td>
|
||||
<td>{row.title}</td>
|
||||
<td>{row.slug}</td>
|
||||
<td>{row.description}</td>
|
||||
<td>{row.run_every}s</td>
|
||||
<td>
|
||||
{format(row.last_run * 1000, 'PP HH:mm')}<br />
|
||||
{formatDistance(row.last_run * 1000, new Date(), { addSuffix: true })}
|
||||
</td>
|
||||
<td>
|
||||
{format(row.next_run * 1000, 'PP HH:mm')}<br />
|
||||
{formatDistance(row.next_run * 1000, new Date(), { addSuffix: true })}
|
||||
</td>
|
||||
<td>{row.run_time}</td>
|
||||
<td>{row.cron_state ? 'RUNNING' : 'IDLE'}</td>
|
||||
<td>{row.is_enabled ? 'ENABLED' : 'DISABLED'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mb-2">
|
||||
<DtSrRowCount {handler} />
|
||||
</div>
|
||||
</div>
|
|
@ -1,26 +0,0 @@
|
|||
import { apiUri } from '$lib/utils/common';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
/** @param {import('@vincjo/datatables/remote/state')} state */
|
||||
export const loadData = async (state) => {
|
||||
const response = await fetch(apiUri(`/api/v1/crons?${getParams(state)}`));
|
||||
const json = await response.json();
|
||||
if (json.data === null) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
state.setTotalRows(json.data.length ?? 0);
|
||||
return json.data.items ?? [];
|
||||
};
|
||||
|
||||
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
|
||||
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
|
||||
|
||||
if (sort) {
|
||||
params += `&sort_by=${sort.orderBy}&sort_direction=${sort.direction}`;
|
||||
}
|
||||
if (filters) {
|
||||
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
|
||||
}
|
||||
return params;
|
||||
};
|
|
@ -1,189 +0,0 @@
|
|||
<script>
|
||||
import { DataHandler } from '@vincjo/datatables/remote';
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
import { loadData, createProber, editProber, deleteProber } from './api-handler';
|
||||
import { onMount } from 'svelte';
|
||||
import { getModalStore, getToastStore } from '@skeletonlabs/skeleton';
|
||||
import {
|
||||
DtSrRowsPerPage,
|
||||
DtSrThSort,
|
||||
DtSrThFilter,
|
||||
DtSrRowCount,
|
||||
DtSrPagination,
|
||||
DtSrAutoRefresh
|
||||
} from '$lib/components/datatables/server';
|
||||
const modalStore = getModalStore();
|
||||
const toastStore = getToastStore();
|
||||
|
||||
function showAddModal() {
|
||||
/** @type {import('@skeletonlabs/skeleton').ModalSettings} */
|
||||
const modal = {
|
||||
type: 'prompt',
|
||||
// Data
|
||||
title: 'Enter Name',
|
||||
body: 'Enter a name for the prober',
|
||||
valueAttr: { type: 'text', minlength: 3, maxlength: 50, required: true },
|
||||
response: (r) => {
|
||||
if (!r) return;
|
||||
createProber(r)
|
||||
.then((res) => {
|
||||
if (res.status !== 'ok') {
|
||||
toastStore.trigger({ message: 'Failed to create prober' });
|
||||
} else {
|
||||
toastStore.trigger({
|
||||
message: 'Prober created',
|
||||
background: 'variant-filled-success'
|
||||
});
|
||||
handler.invalidate();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toastStore.trigger({ message: 'Failed to create prober' });
|
||||
});
|
||||
}
|
||||
};
|
||||
modalStore.trigger(modal);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} proberId
|
||||
* @param {string} proberName
|
||||
*/
|
||||
function showEditModal(proberId, proberName) {
|
||||
/** @type {import('@skeletonlabs/skeleton').ModalSettings} */
|
||||
const modal = {
|
||||
type: 'prompt',
|
||||
// Data
|
||||
title: 'Enter Name',
|
||||
body: 'Enter a new name for the prober',
|
||||
value: proberName,
|
||||
valueAttr: { type: 'text', minlength: 3, maxlength: 50, required: true },
|
||||
response: (r) => {
|
||||
if (!r) return;
|
||||
editProber(proberId, r)
|
||||
.then((res) => {
|
||||
if (res.status !== 'ok') {
|
||||
toastStore.trigger({ message: 'Failed to edit prober' });
|
||||
} else {
|
||||
toastStore.trigger({
|
||||
message: 'Prober edited',
|
||||
background: 'variant-filled-success'
|
||||
});
|
||||
handler.invalidate();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toastStore.trigger({ message: 'Failed to edit prober' });
|
||||
});
|
||||
}
|
||||
};
|
||||
modalStore.trigger(modal);
|
||||
}
|
||||
|
||||
/** @param {number} id */
|
||||
const handleDelete = (id) => {
|
||||
modalStore.trigger({
|
||||
type: 'confirm',
|
||||
title: 'Please Confirm',
|
||||
body: 'Are you sure you wish to proceed?',
|
||||
/** @param {boolean} r */
|
||||
response: async (r) => {
|
||||
if (r) {
|
||||
deleteProber(id)
|
||||
.then((res) => {
|
||||
if (res.status !== 'ok') {
|
||||
toastStore.trigger({ message: 'Failed to delete prober' });
|
||||
} else {
|
||||
toastStore.trigger({
|
||||
message: 'Prober deleted',
|
||||
background: 'variant-filled-success'
|
||||
});
|
||||
handler.invalidate();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toastStore.trigger({ message: 'Prober could not be deleted' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
|
||||
let rows = handler.getRows();
|
||||
|
||||
onMount(() => {
|
||||
handler.onChange((state) => loadData(state));
|
||||
handler.invalidate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-4">
|
||||
<h1 class="h2 font-extrabold dark:text-white">Prober</h1>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<button class="variant-filled-success btn btn-sm mb-4" on:click={showAddModal}>Add Prober</button>
|
||||
<div class="flex justify-between">
|
||||
<DtSrRowsPerPage {handler} />
|
||||
<div class="invisible flex place-items-center md:visible">
|
||||
<DtSrAutoRefresh {handler} />
|
||||
</div>
|
||||
<div class="flex place-items-center">
|
||||
<button
|
||||
id="reloadDt"
|
||||
name="reloadDt"
|
||||
class="variant-filled-primary btn"
|
||||
on:click={() => handler.invalidate()}>Reload</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-2 overflow-x-auto">
|
||||
<table class="table table-hover table-compact w-full table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<DtSrThSort {handler} orderBy="id">ID</DtSrThSort>
|
||||
<th>Name</th>
|
||||
<th>API Key</th>
|
||||
<DtSrThSort {handler} orderBy="last_submit_ts">Last Submit</DtSrThSort>
|
||||
</tr>
|
||||
<tr>
|
||||
<DtSrThFilter {handler} filterBy="name" placeholder="Name" colspan={2} />
|
||||
<DtSrThFilter {handler} filterBy="api_key" placeholder="API Key" colspan={2} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $rows as row (row.id)}
|
||||
<tr>
|
||||
<td>
|
||||
{row.id}
|
||||
<button
|
||||
class="variant-filled-secondary btn btn-sm mr-1"
|
||||
on:click={() => showEditModal(row.id, row.name)}>Edit</button
|
||||
>
|
||||
<button
|
||||
class="variant-filled-error btn btn-sm"
|
||||
name="delete_{row.id}"
|
||||
on:click={() => {
|
||||
handleDelete(row.id);
|
||||
}}>Delete</button
|
||||
>
|
||||
</td>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.api_key}</td>
|
||||
<td>
|
||||
{format(row.last_submit_ts * 1000, 'PP HH:mm')}<br />
|
||||
{formatDistance(row.last_submit_ts * 1000, new Date(), { addSuffix: true })}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mb-2">
|
||||
<DtSrRowCount {handler} />
|
||||
<DtSrPagination {handler} />
|
||||
</div>
|
||||
</div>
|
|
@ -1,61 +0,0 @@
|
|||
import { apiUri } from '$lib/utils/common';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
/** @param {import('@vincjo/datatables/remote/state')} state */
|
||||
export const loadData = async (state) => {
|
||||
const response = await fetch(apiUri(`/api/v1/prober?${getParams(state)}`));
|
||||
const json = await response.json();
|
||||
if (json.data === null) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
state.setTotalRows(json.data.total_rows ?? 0);
|
||||
return json.data.items ?? [];
|
||||
};
|
||||
|
||||
export const createProber = async (name) => {
|
||||
const response = await fetch(apiUri('/api/v1/prober'), {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
const json = await response.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
export const editProber = async (id, name) => {
|
||||
const response = await fetch(apiUri(`/api/v1/prober/${id}`), {
|
||||
method: 'PATCH',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
const json = await response.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
export const deleteProber = async (id) => {
|
||||
const response = await fetch(apiUri(`/api/v1/prober/${id}`), {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
const json = await response.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
|
||||
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
|
||||
|
||||
if (sort) {
|
||||
params += `&sort_by=${sort.orderBy}&sort_direction=${sort.direction}`;
|
||||
}
|
||||
if (filters) {
|
||||
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
|
||||
}
|
||||
return params;
|
||||
};
|
|
@ -1,10 +1,18 @@
|
|||
<script>
|
||||
import '../app.css';
|
||||
import { page } from '$app/stores';
|
||||
import { Toast, Modal } from '@skeletonlabs/skeleton';
|
||||
import {
|
||||
Toast,
|
||||
Modal,
|
||||
Drawer,
|
||||
ProgressBar,
|
||||
initializeStores,
|
||||
storePopup
|
||||
} from '@skeletonlabs/skeleton';
|
||||
import { beforeNavigate, afterNavigate } from '$app/navigation';
|
||||
import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom';
|
||||
import { ProgressBar, initializeStores, storePopup } from '@skeletonlabs/skeleton';
|
||||
import { MainNav, MobileDrawer } from '$lib/components/navigation';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
|
||||
initializeStores();
|
||||
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
|
||||
|
@ -60,4 +68,18 @@
|
|||
meter="bg-gradient-to-br from-purple-600 via-pink-600 to-blue-600"
|
||||
/>
|
||||
{/if}
|
||||
<slot />
|
||||
|
||||
<Drawer>
|
||||
<h2 class="p-4">Navigation</h2>
|
||||
<hr />
|
||||
<MobileDrawer />
|
||||
<hr />
|
||||
</Drawer>
|
||||
|
||||
<MainNav />
|
||||
|
||||
<div class="pt-10 md:pt-12 min-h-screen">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
<script>
|
||||
import { invalidateAll, goto } from '$app/navigation';
|
||||
import { apiUri } from '$lib/utils/common';
|
||||
import { ProgressBar, LightSwitch } from '@skeletonlabs/skeleton';
|
||||
|
||||
/** @type {ApiResponse} */
|
||||
export let formResult;
|
||||
|
||||
let isProcessing = false;
|
||||
|
||||
/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
|
||||
async function handleSubmit(event) {
|
||||
isProcessing = true;
|
||||
const data = new FormData(event.currentTarget);
|
||||
|
||||
const response = await fetch(event.currentTarget.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: JSON.stringify(Object.fromEntries(data))
|
||||
});
|
||||
|
||||
formResult = await response.json();
|
||||
isProcessing = false;
|
||||
|
||||
if (formResult.status === 'ok') {
|
||||
await invalidateAll();
|
||||
goto('/app/prober/');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<a href="/" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white"
|
||||
>XMR Nodes</a
|
||||
>
|
||||
<div
|
||||
class="w-full rounded-lg shadow border md:mt-0 sm:max-w-md xl:p-0 bg-white border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<!-- prettier-ignore -->
|
||||
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<h1 class="text-xl font-bold leading-tight tracking-tight tmd:text-2xl text-gray-900 dark:text-white">Sign in to your account</h1>
|
||||
<form class="space-y-4 md:space-y-6" action={apiUri('/auth/login')} method="POST" on:submit|preventDefault={handleSubmit}
|
||||
>
|
||||
<div>
|
||||
<label for="username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||
<input type="text" name="username" id="username" class="input" placeholder="username" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
<input type="password" name="password" id="password" placeholder="••••••••" class="input" required />
|
||||
</div>
|
||||
<button type="submit" class="btn variant-filled-primary w-full">Sign in</button>
|
||||
<LightSwitch />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{#if !isProcessing}
|
||||
{#if formResult?.status === 'error'}
|
||||
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-red-400" role="alert">
|
||||
<span class="font-medium">Error:</span>
|
||||
{formResult.message}!
|
||||
</div>
|
||||
{/if}
|
||||
{#if formResult?.status === 'ok'}
|
||||
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-green-400" role="alert">
|
||||
<span class="font-medium">Success:</span>
|
||||
{formResult.message}!
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<ProgressBar meter="bg-secondary-500" track="bg-secondary-500/30" value={undefined} />
|
||||
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-blue-400" role="alert">
|
||||
<span class="font-medium">Processing...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
Loading…
Reference in a new issue