Files
UpSnap-seriousm4x-4/frontend/src/components/DeviceForm.svelte
2023-01-30 01:21:03 +01:00

397 lines
16 KiB
Svelte

<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { pocketbase } from '@stores/pocketbase';
import { faEye, faEyeSlash, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import Fa from 'svelte-fa';
export let device;
export let mode;
let pb;
let timeout;
let button = {
state: 'none',
error: ''
};
let deleteButton = {
state: 'none',
error: ''
};
let newPort = {
name: '',
number: null
};
let passwordShow = false;
$: passwordType = passwordShow ? 'text' : 'password';
onMount(async () => {
pocketbase.subscribe((conn) => {
pb = conn;
});
});
async function addOrUpdateDevice() {
button.state = 'waiting';
try {
// validate password length
if (
device.password.length != 0 &&
device.password.length != 4 &&
device.password.length != 6
) {
throw 'Password must be 0, 4 or 6 characters long';
}
// add ports not in db
for (let i = 0; i < device.expand.ports.length; i++) {
const port = device.expand.ports[i];
if (!port.id) {
const result = await pb.collection('ports').create(port);
device.ports = [...device.ports, result.id];
}
}
// create or update device
if (mode === 'add') {
await pb.collection('devices').create(device);
} else {
await pb.collection('devices').update(device.id, device);
}
// show button with timeout
clearTimeout(timeout);
timeout = setTimeout(() => {
button.state = 'none';
}, 3000);
button.state = 'success';
} catch (error) {
clearTimeout(timeout);
setTimeout(() => {
button.error = '';
button.state = 'none';
}, 3000);
button.error = error;
button.state = 'failed';
}
}
async function deleteDevice() {
deleteButton.state = 'waiting';
try {
device.ports.forEach(async (port) => {
await pb.collection('ports').delete(port);
});
await pb.collection('devices').delete(device.id);
goto('/');
} catch (error) {
clearTimeout(timeout);
timeout = setTimeout(() => {
deleteButton.error = '';
deleteButton.state = 'none';
}, 3000);
deleteButton.error = error;
deleteButton.state = 'failed';
}
}
async function deletePort(idx) {
// delete port from db if it has an id
// ports with id exist in db, ports without id are not created yet
const port = device.expand.ports[idx];
if (port.id) {
await pb.collection('ports').delete(port.id);
const i = device.ports.indexOf(port.id);
if (i !== -1) {
device.ports.splice(i, 1);
device.ports = device.ports;
}
}
device.expand.ports.splice(idx, 1);
device.expand.ports = device.expand.ports;
}
function addPort() {
device.expand.ports = [...device.expand.ports, JSON.parse(JSON.stringify(newPort))];
newPort.name = '';
newPort.number = null;
}
function onPasswordInput(event) {
device.password = event.target.value;
}
</script>
<section class="m-0 mt-4 p-4 shadow-sm">
<h3 class="mb-3">{mode === 'add' ? 'Add new device' : device.name}</h3>
<div class="row">
<div class="col-md-6">
<form on:submit|preventDefault={addOrUpdateDevice}>
<h5>Required:</h5>
<div class="input-group mb-1">
<span class="input-group-text">Name</span>
<input
class="form-control"
placeholder="Office Pc"
aria-label="Name"
aria-describedby="addon-wrapping"
type="text"
required
bind:value={device.name}
/>
</div>
<div class="input-group mb-1">
<span class="input-group-text">IP</span>
<input
class="form-control"
placeholder="192.168.1.34"
aria-label="IP"
aria-describedby="addon-wrapping"
type="text"
required
bind:value={device.ip}
/>
</div>
<div class="input-group mb-1">
<span class="input-group-text">MAC</span>
<input
class="form-control"
placeholder="aa:bb:cc:dd:ee:ff"
aria-label="MAC"
aria-describedby="addon-wrapping"
type="text"
required
bind:value={device.mac}
/>
</div>
<div class="input-group">
<span class="input-group-text">Netmask</span>
<input
class="form-control"
placeholder="Most likely 255.255.255.0 or 255.255.0.0"
aria-label="Netmask"
aria-describedby="addon-wrapping"
type="text"
required
bind:value={device.netmask}
/>
</div>
<h5 class="mt-4">Optional:</h5>
{#if device?.expand?.ports}
{#each device.expand.ports as port, idx}
<div class="input-group mb-1">
<span class="input-group-text">Port</span>
<input
type="text"
aria-label="Name"
class="form-control"
placeholder="Name"
bind:value={port.name}
/>
<input
type="number"
max="65535"
aria-label="Number"
class="form-control"
placeholder="Number"
bind:value={port.number}
/>
<button
type="button"
class="btn btn-outline-secondary"
on:click={() => deletePort(idx)}
>
<Fa icon={faTrashCan} />
</button>
</div>
{/each}
{/if}
<div class="input-group mb-3">
<span class="input-group-text">Port</span>
<input
type="text"
aria-label="Name"
class="form-control"
placeholder="Name"
bind:value={newPort.name}
/>
<input
type="number"
max="65535"
aria-label="Number"
class="form-control"
placeholder="Number"
bind:value={newPort.number}
/>
<button
type="button"
class="btn btn-outline-secondary"
on:click={() => addPort()}
>
<Fa icon={faPlus} />
</button>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Link</span>
<input
class="form-control"
placeholder="Clickable link on device card"
aria-label="Link"
aria-describedby="addon-wrapping"
type="url"
bind:value={device.link}
/>
</div>
<div class="input-group mb-1">
<span class="input-group-text">Wake Cron<sup>(1)</sup></span>
<input
class="form-control"
placeholder="Automatically wake device with cron"
aria-label="Wake Cron"
aria-describedby="addon-wrapping"
type="text"
bind:value={device.wake_cron}
/>
<div class="input-group-text form-switch">
<label class="form-check-label" for="wake-cron-enabled">Enable</label>
<input
id="wake-cron-enabled"
class="form-check-input mt-0 ms-1"
type="checkbox"
role="switch"
aria-label="Enable/Disable wake cron"
bind:checked={device.wake_cron_enabled}
/>
</div>
</div>
<div class="input-group mb-1">
<span class="input-group-text">Shutdown Cron<sup>(1)</sup></span>
<input
class="form-control"
placeholder="Automatically shutdown device with cron"
aria-label="Shutdown Cron"
aria-describedby="addon-wrapping"
type="text"
bind:value={device.shutdown_cron}
/>
<div class="input-group-text form-switch">
<label class="form-check-label" for="wake-shutdown-enabled">Enable</label>
<input
id="wake-shutdown-enabled"
class="form-check-input mt-0 ms-1"
type="checkbox"
role="switch"
aria-label="Enable/Disable wake cron"
bind:checked={device.shutdown_cron_enabled}
/>
</div>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Shutdown Cmd<sup>(2)</sup></span>
<input
class="form-control"
placeholder="Command to shutdown device"
aria-label="Shutdown Cmd"
aria-describedby="addon-wrapping"
type="text"
bind:value={device.shutdown_cmd}
/>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Password<sup>(3)</sup></span>
<input
class="form-control"
placeholder="BIOS password for wol"
aria-label="IP"
aria-describedby="addon-wrapping"
type={passwordType}
value={device.password}
maxlength="6"
on:input={onPasswordInput}
/>
<button
class="btn btn-outline-secondary"
type="button"
on:click={() => (passwordShow = !passwordShow)}
>
<Fa icon={passwordShow ? faEyeSlash : faEye} />
</button>
</div>
<button
type="submit"
class="btn btn-secondary"
class:btn-success={button.state === 'success' ? true : false}
class:btn-warning={button.state === 'waiting' ? true : false}
class:btn-danger={button.state === 'failed' ? true : false}
disabled={button.state !== 'none' ? true : false}
>
{#if button.state === 'none'}
{mode === 'add' ? 'Add device' : 'Save device'}
{:else if button.state === 'success'}
{mode === 'add' ? 'Added successfully' : 'Saved successfully'}
{:else if button.state === 'waiting'}
Waiting
{:else if button.state === 'failed'}
Failed: {button.error}
{/if}
</button>
</form>
</div>
<div class="col-md-6 d-flex align-items-start flex-column">
<div class="callout callout-info mb-auto">
<h5>Optional:</h5>
<p class="m-0">
(1) Read more about valid cron syntax on <a
href="https://en.wikipedia.org/wiki/Cron"
target="_blank"
rel="noreferrer">wikipedia</a
>
or the package documentation
<a
target="_blank"
rel="noreferrer"
href="https://pkg.go.dev/github.com/robfig/cron"
>pkg.go.dev/github.com/robfig/cron</a
>
</p>
<p class="m-0">
(2) Shell command to be executed. "net rpc", "sshpass" and "curl" are available.
e.g.:
</p>
<ul>
<li>
Windows: <code>net rpc shutdown -I 192.168.1.13 -U "user%password"</code>
</li>
<li>
Linux: <code
>sshpass -p your_password ssh -o "StrictHostKeyChecking=no"
user@hostname "sudo poweroff"</code
>
</li>
</ul>
<p class="m-0">
(3) Some network cards have the option to set a password for magic packets, also
called "SecureON". Password can only be 0, 4 or 6 characters in length.
</p>
</div>
{#if mode === 'edit'}
<div class="text-end mt-3 align-self-end">
<button
class="btn btn-danger"
on:click={() => deleteDevice()}
on:keydown={() => deleteDevice()}
>
{#if deleteButton.state === 'none'}
Delete device
{:else if deleteButton.state === 'waiting'}
Waiting
{:else if deleteButton.state === 'failed'}
Failed: {deleteButton.error}
{/if}
</button>
</div>
{/if}
</div>
</div>
</section>