removing admin UI and layout #2

This become only one layout for Web UI
This commit is contained in:
Cristian Ditaputratama 2024-05-18 20:36:33 +07:00
parent d5f510ae32
commit d13a427c6d
Signed by: ditatompel
GPG key ID: 31D3D06D77950979
18 changed files with 25 additions and 533 deletions

View file

@ -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 />

View file

@ -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>

View file

@ -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>

View file

@ -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;
};

View file

@ -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>

View file

@ -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;
};

View file

@ -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 />

View file

@ -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>