Added some quick commands to WebUI backends page.

This commit is contained in:
Abdulmhsen B. A. A.
2024-06-26 18:56:53 +03:00
parent 89e40c0654
commit dabbf317a3
4 changed files with 92 additions and 52 deletions

View File

@@ -85,7 +85,7 @@
</div> </div>
</div> </div>
</div> </div>
<footer class="card-footer"> <div class="card-footer">
<div class="card-footer-item" v-if="backend.export.enabled"> <div class="card-footer-item" v-if="backend.export.enabled">
<NuxtLink class="button is-danger is-fullwidth" <NuxtLink class="button is-danger is-fullwidth"
:to="makeConsoleCommand(`state:export -v -s ${backend.name}`)"> :to="makeConsoleCommand(`state:export -v -s ${backend.name}`)">
@@ -100,8 +100,8 @@
<span>Run import now</span> <span>Run import now</span>
</NuxtLink> </NuxtLink>
</div> </div>
</footer> </div>
<footer class="card-footer"> <div class="card-footer">
<div class="card-footer-item"> <div class="card-footer-item">
<div class="field"> <div class="field">
<input :id="backend.name+'_export'" type="checkbox" class="switch is-success" <input :id="backend.name+'_export'" type="checkbox" class="switch is-success"
@@ -125,6 +125,18 @@
<span class="is-hidden-tablet">Webhook</span> <span class="is-hidden-tablet">Webhook</span>
</NuxtLink> </NuxtLink>
</div> </div>
</div>
<footer class="card-footer">
<div class="card-footer-item">
<div class="select is-fullwidth">
<select v-model="selectedCommand" @change="forwardCommand(backend)">
<option value="" disabled>Quick commands</option>
<option v-for="(command, index) in usefulCommands" :key="`command_${index}`" :value="index">
{{ command.title }}
</option>
</select>
</div>
</div>
</footer> </footer>
</div> </div>
</div> </div>
@@ -151,7 +163,7 @@ import 'assets/css/bulma-switch.css'
import moment from 'moment' import moment from 'moment'
import request from '~/utils/request.js' import request from '~/utils/request.js'
import BackendAdd from '~/components/BackendAdd.vue' import BackendAdd from '~/components/BackendAdd.vue'
import {copyText, makeConsoleCommand, notification, TOOLTIP_DATE_FORMAT} from '~/utils/index.js' import {copyText, makeConsoleCommand, notification, r, TOOLTIP_DATE_FORMAT} from '~/utils/index.js'
import {useStorage} from "@vueuse/core"; import {useStorage} from "@vueuse/core";
import Message from "~/components/Message.vue"; import Message from "~/components/Message.vue";
@@ -162,6 +174,36 @@ const toggleForm = ref(false)
const api_url = useStorage('api_url', '') const api_url = useStorage('api_url', '')
const show_page_tips = useStorage('show_page_tips', true) const show_page_tips = useStorage('show_page_tips', true)
const isLoading = ref(false) const isLoading = ref(false)
const selectedCommand = ref('')
const usefulCommands = [
{
title: "Force export local play state to this backend.",
command: 'state:export -fi -v -s {name}'
},
{
title: "Backup this backend play state.",
command: "state:backup -v -s {name} --file '{date}.manual_{name}.json'",
},
{
title: "Import backend metadata only.",
command: "state:import -v --metadata-only -s {name}",
},
]
const forwardCommand = async backend => {
if ('' === selectedCommand.value) {
return
}
const Index = selectedCommand.value
selectedCommand.value = ''
const util = {
date: moment().format('YYYYMMDD'),
}
await navigateTo(makeConsoleCommand(r(usefulCommands[Index].command, {...backend, ...util})));
}
const loadContent = async () => { const loadContent = async () => {
backends.value = [] backends.value = []
@@ -191,5 +233,4 @@ const updateValue = async (backend, key, newValue) => {
backends.value[backends.value.findIndex(b => b.name === backend.name)] = await response.json() backends.value[backends.value.findIndex(b => b.name === backend.name)] = await response.json()
} }
</script> </script>

View File

@@ -379,30 +379,23 @@ const makePagination = (current, last, delta = 5) => {
const strR = '-'.repeat(9 + `${last}`.length) const strR = '-'.repeat(9 + `${last}`.length)
const left = current - delta, const left = current - delta, right = current + delta + 1;
right = current + delta + 1;
for (let i = 1; i <= last; i++) { for (let i = 1; i <= last; i++) {
if (i === 1 || i === last || (i >= left && i < right)) { if (i === 1 || i === last || (i >= left && i < right)) {
if (i === left && i > 2) { if (i === left && i > 2) {
pagination.push({ pagination.push({
page: 0, page: 0, text: strR, selected: false,
text: strR,
selected: false,
}); });
} }
pagination.push({ pagination.push({
page: i, page: i, text: `Page #${i}`, selected: i === current
text: `Page #${i}`,
selected: i === current
}); });
if (i === right - 1 && i < last - 1) { if (i === right - 1 && i < last - 1) {
pagination.push({ pagination.push({
page: 0, page: 0, text: strR, selected: false,
text: strR,
selected: false,
}); });
} }
} }
@@ -412,6 +405,7 @@ const makePagination = (current, last, delta = 5) => {
} }
export { export {
r,
ag_set, ag_set,
ag, ag,
humanFileSize, humanFileSize,

View File

@@ -26,9 +26,9 @@ use Throwable;
#[Cli(command: self::ROUTE)] #[Cli(command: self::ROUTE)]
class BackupCommand extends Command class BackupCommand extends Command
{ {
public const ROUTE = 'state:backup'; public const string ROUTE = 'state:backup';
public const TASK_NAME = 'backup'; public const string TASK_NAME = 'backup';
/** /**
* Constructs a new instance of the class. * Constructs a new instance of the class.
@@ -180,31 +180,35 @@ class BackupCommand extends Command
$type = strtolower(ag($backend, 'type', 'unknown')); $type = strtolower(ag($backend, 'type', 'unknown'));
if ($isCustom && $input->getOption('exclude') === in_array($backendName, $selected, true)) { if ($isCustom && $input->getOption('exclude') === in_array($backendName, $selected, true)) {
$this->logger->info('SYSTEM: Ignoring [{backend}] as requested by select backends flag.', [ $this->logger->info("SYSTEM: Ignoring '{backend}' as requested by [-s, --select-backend].", [
'backend' => $backendName, 'backend' => $backendName
]); ]);
continue; continue;
} }
if (true !== (bool)ag($backend, 'import.enabled')) { if (true !== (bool)ag($backend, 'import.enabled')) {
$this->logger->info('SYSTEM: Ignoring [{backend}] imports are disabled for this backend.', [ $this->logger->info("SYSTEM: Ignoring '{backend}' as the backend has import disabled.", [
'backend' => $backendName, 'backend' => $backendName
]); ]);
continue; continue;
} }
if (!isset($supported[$type])) { if (!isset($supported[$type])) {
$this->logger->error('SYSTEM: Ignoring [{backend}] because of the unexpected type [{type}].', [ $this->logger->error(
'type' => $type, "SYSTEM: Ignoring '{backend}' due to unexpected type '{type}'. Expecting '{types}'.",
'backend' => $backendName, [
]); 'type' => $type,
'backend' => $backendName,
'types' => implode(', ', array_keys($supported)),
]
);
continue; continue;
} }
if (null === ($url = ag($backend, 'url')) || false === isValidURL($url)) { if (null === ($url = ag($backend, 'url')) || false === isValidURL($url)) {
$this->logger->error('SYSTEM: Ignoring [{backend}] because of invalid URL.', [ $this->logger->error("SYSTEM: Ignoring '{backend}' due to invalid URL. '{url}'.", [
'backend' => $backendName,
'url' => $url ?? 'None', 'url' => $url ?? 'None',
'backend' => $backendName,
]); ]);
continue; continue;
} }
@@ -221,7 +225,7 @@ class BackupCommand extends Command
} }
if (true !== $input->getOption('no-enhance')) { if (true !== $input->getOption('no-enhance')) {
$this->logger->notice('SYSTEM: Preloading {mapper} data.', [ $this->logger->notice("SYSTEM: Preloading '{mapper}' data.", [
'mapper' => afterLast($this->mapper::class, '\\'), 'mapper' => afterLast($this->mapper::class, '\\'),
'memory' => [ 'memory' => [
'now' => getMemoryUsage(), 'now' => getMemoryUsage(),
@@ -229,10 +233,12 @@ class BackupCommand extends Command
], ],
]); ]);
$start = microtime(true);
$this->mapper->loadData(); $this->mapper->loadData();
$this->logger->notice('SYSTEM: Preloading {mapper} data is complete.', [ $this->logger->notice("SYSTEM: Preloading '{mapper}' data completed in '{duration}s'.", [
'mapper' => afterLast($this->mapper::class, '\\'), 'mapper' => afterLast($this->mapper::class, '\\'),
'duration' => round(microtime(true) - $start, 2),
'memory' => [ 'memory' => [
'now' => getMemoryUsage(), 'now' => getMemoryUsage(),
'peak' => getPeakMemoryUsage(), 'peak' => getPeakMemoryUsage(),
@@ -243,7 +249,7 @@ class BackupCommand extends Command
/** @var array<array-key,ResponseInterface> $queue */ /** @var array<array-key,ResponseInterface> $queue */
$queue = []; $queue = [];
$this->logger->notice('Using WatchState Version - \'{version}\'.', ['version' => getAppVersion()]); $this->logger->notice("Using WatchState version - '{version}'.", ['version' => getAppVersion()]);
foreach ($list as $name => &$backend) { foreach ($list as $name => &$backend) {
$opts = ag($backend, 'options', []); $opts = ag($backend, 'options', []);
@@ -275,8 +281,8 @@ class BackupCommand extends Command
$fileName = Config::get('path') . '/backup/{backend}.json'; $fileName = Config::get('path') . '/backup/{backend}.json';
} }
if (count($list) <= 1 && null === ($file = $input->getOption('file'))) { if (count($list) <= 1 && null !== ($file = $input->getOption('file'))) {
$fileName = $file; $fileName = str_starts_with($file, '/') ? $file : Config::get('path') . '/backup' . '/' . $file;
} }
if (false === $input->getOption('dry-run')) { if (false === $input->getOption('dry-run')) {
@@ -289,6 +295,11 @@ class BackupCommand extends Command
touch($fileName); touch($fileName);
} }
$this->logger->notice("SYSTEM: '{backend}' is using '{file}' as backup target.", [
'file' => realpath($fileName),
'backend' => $name,
]);
$backend['fp'] = new Stream($fileName, 'wb+'); $backend['fp'] = new Stream($fileName, 'wb+');
$backend['fp']->write('['); $backend['fp']->write('[');
} }
@@ -308,12 +319,9 @@ class BackupCommand extends Command
unset($backend); unset($backend);
$start = makeDate(); $start = microtime(true);
$this->logger->notice('SYSTEM: Waiting on [{total}] requests.', [ $this->logger->notice("SYSTEM: Waiting on '{total}' requests.", [
'total' => number_format(count($queue)), 'total' => number_format(count($queue)),
'time' => [
'start' => $start,
],
'memory' => [ 'memory' => [
'now' => getMemoryUsage(), 'now' => getMemoryUsage(),
'peak' => getPeakMemoryUsage(), 'peak' => getPeakMemoryUsage(),
@@ -347,13 +355,8 @@ class BackupCommand extends Command
} }
} }
$end = makeDate(); $this->logger->notice("SYSTEM: Backup operation finished in '{duration}s'.", [
$this->logger->notice('SYSTEM: Operation is finished.', [ 'duration' => round(microtime(true) - $start, 2),
'time' => [
'start' => $start,
'end' => $end,
'duration' => $end->getTimestamp() - $start->getTimestamp(),
],
'memory' => [ 'memory' => [
'now' => getMemoryUsage(), 'now' => getMemoryUsage(),
'peak' => getPeakMemoryUsage(), 'peak' => getPeakMemoryUsage(),

View File

@@ -42,21 +42,23 @@ final class Stream implements StreamInterface, Stringable
$resource = $stream; $resource = $stream;
if (is_string($stream)) { if (is_string($stream)) {
set_error_handler(function ($e) use (&$error) { set_error_handler(function ($severity, $message, $file, $line) use (&$error) {
if ($e !== E_WARNING) { if ($severity !== E_WARNING) {
return; return;
} }
$error = $e; $error = r("Stream: Failed to open stream. '{message}' at '{file}:{line}'", [
'message' => trim($message),
'file' => $file,
'line' => $line
]);
}); });
$resource = fopen($stream, $mode); $resource = fopen($stream, $mode);
restore_error_handler(); restore_error_handler();
} }
if ($error) { if (null !== $error) {
throw new RuntimeException(r('Stream: Invalid stream reference provided. Error {error}.', [ throw new RuntimeException($error);
'error' => ag(error_get_last() ?? [], 'message', '??'),
]));
} }
if (!self::isValidStreamResourceType($resource)) { if (!self::isValidStreamResourceType($resource)) {