Mostly WebUI changes to make easier to filter stuff

This commit is contained in:
arabcoders
2025-04-04 01:10:10 +03:00
parent c921430651
commit ad2f7b389e
9 changed files with 172 additions and 61 deletions

View File

@@ -149,7 +149,7 @@
<select v-model="selectedCommand" @change="forwardCommand(backend)">
<option value="" disabled>Frequently used commands</option>
<option v-for="(command, index) in usefulCommands" :key="`qc-${index}`" :value="index"
:disabled="false === Boolean(ag(backend, command.state_key, false))">
:disabled="!check_state(backend, command)">
{{ command.id }}. {{ command.title }}
</option>
</select>
@@ -208,31 +208,26 @@ const usefulCommands = {
id: 1,
title: "Run normal export.",
command: 'state:export -v -u {user} -s {name}',
state_key: 'export.enabled',
},
import_now: {
id: 2,
title: "Run normal import.",
command: 'state:import -v -u {user} -s {name}',
state_key: 'import.enabled'
},
force_export: {
id: 3,
title: "Force export local play state to this backend.",
command: 'state:export -fi -v -u {user} -s {name}',
state_key: 'export.enabled',
},
backup_now: {
id: 4,
title: "Backup this backend play state.",
command: "state:backup -v -u {user} -s {name} --file '{date}.manual_{name}.json'",
state_key: 'import.enabled',
},
metadata_only: {
id: 5,
title: "Import this backend metadata.",
command: "state:import -v --metadata-only -u {user} -s {name}",
state_key: 'import.enabled',
},
}
@@ -246,9 +241,10 @@ const forwardCommand = async backend => {
const util = {
date: moment().format('YYYYMMDD'),
user: api_user.value,
}
await navigateTo(makeConsoleCommand(r(usefulCommands[index].command, {...backend, ...util, user: api_user.value})));
await navigateTo(makeConsoleCommand(r(usefulCommands[index].command, {...backend, ...util})));
}
const loadContent = async () => {
@@ -336,4 +332,13 @@ const handleEvents = async (event, backend) => {
}
}
const check_state = (backend, command) => {
if (!command?.state_key) {
return true
}
const state = ag(backend, command.state_key, false)
console.log(backend, command.state_key, state)
return Boolean(state)
}
</script>

View File

@@ -15,6 +15,19 @@
<span>{{ !queued ? 'Queue backup' : 'Remove from queue' }}</span>
</button>
</p>
<div class="control has-icons-left" v-if="toggleFilter || query">
<input type="search" v-model.lazy="query" class="input" id="filter"
placeholder="Filter displayed content">
<span class="icon is-left"><i class="fas fa-filter"/></span>
</div>
<div class="control">
<button class="button is-danger is-light" @click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter"/></span>
</button>
</div>
<p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading"
:class="{ 'is-loading': isLoading }">
@@ -30,21 +43,22 @@
</div>
</div>
<div class="column is-12" v-if="items.length < 1 || isLoading">
<div class="column is-12" v-if="filteredItems.length < 1 || isLoading">
<Message v-if="isLoading" message_class="is-background-info-90 has-text-dark" icon="fas fa-spinner fa-spin"
title="Loading" message="Loading data. Please wait..."/>
<Message v-else title="Warning" message_class="is-background-warning-80 has-text-dark"
icon="fas fa-exclamation-triangle">
No backups found.
<Message v-else
:title="query ? 'Search results' : 'Warning'"
message_class="is-background-warning-80 has-text-dark" icon="fas fa-exclamation-triangle">
<span v-if="query">No results found for <strong>{{ query }}</strong></span>
<span v-else>No backups found.</span>
</Message>
</div>
<div class="column is-6-tablet" v-for="(item, index) in items" :key="'backup-' + index">
<div class="column is-6-tablet" v-for="(item, index) in filteredItems" :key="'backup-' + index">
<div class="card">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<span class="icon"><i class="fas fa-download"
:class="{ 'fa-spin': item?.isDownloading }"></i>&nbsp;</span>
<span class="icon"><i class="fas fa-download" :class="{ 'fa-spin': item?.isDownloading }"/>&nbsp;</span>
<span>
<NuxtLink @click="downloadFile(item)" v-text="item.filename"/>
</span>
@@ -112,9 +126,8 @@
<li>
To generate a manual backup, go to the
<NuxtLink to="/backends"><span class="icon"><i class="fas fa-server"></i></span> Backends</NuxtLink>
page and from the drop down menu select the 4th option `Backup this backend play state`, or via cli
using
<code>state:backup</code> command from the console. or by <span class="icon"><i
page and from the drop down menu select the 4th option <code>Backup this backend play state</code>, or via
cli using <code>state:backup</code> command from the console. or by <span class="icon"><i
class="fas fa-terminal"/></span>
<NuxtLink :to="makeConsoleCommand('state:backup -s [backend] --file /config/backup/[file]')"
v-text="'Web Console'"/>
@@ -139,13 +152,29 @@ import {humanFileSize, makeConsoleCommand, notification, TOOLTIP_DATE_FORMAT} fr
import Message from '~/components/Message'
import {useStorage} from '@vueuse/core'
const route = useRoute()
useHead({title: 'Backups'})
const items = ref([])
const isLoading = ref(false)
const queued = ref(true)
const show_page_tips = useStorage('show_page_tips', true)
const api_user = useStorage('api_user', 'main')
const users = ref([])
const query = ref(route.query.filter ?? '')
const toggleFilter = ref(false)
watch(toggleFilter, () => {
if (!toggleFilter.value) {
query.value = ''
}
});
const filteredItems = computed(() => {
if (!query.value) {
return items.value
}
return items.value.filter(item => item.filename.toLowerCase().includes(query.value.toLowerCase()))
})
const loadContent = async () => {
items.value = []

View File

@@ -78,8 +78,9 @@
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
You can also run a command from the task page by clicking on the <strong>Run via console</strong>. The
command will be pre-filled for you.
You don't need to write <code>console</code> or <code>docker exec -ti watchstate console</code> Using
this interface. Use the command followed by the options directly. For example, <code>db:list --output
yaml</code>.
</li>
<li>
Clicking close connection does not stop the command. It only stops the output from being displayed. The
@@ -87,12 +88,8 @@
</li>
<li>
The majority of the commands will not show any output unless error has occurred or important information
needs to be communicated. To see all output, add the <code>-vvv</code> flag.
</li>
<li>
There is no need to write <code>console</code> or <code>docker exec -ti watchstate console</code> Using
this interface. Use the command followed by the options directly. For example, <code>db:list --output
yaml</code>.
needs to be communicated. Use the <code>-v[v[v]]</code> option to increase verbosity. <code>-v</code>
should be enough for most people, If you are debugging, then use <code>-vv --context</code>.
</li>
<li>
There is an environment variable <code>WS_CONSOLE_ENABLE_ALL</code> that can be set to <code>true</code>
@@ -134,6 +131,9 @@ const outputConsole = ref()
const command_input = ref()
const executedCommands = useStorage('executedCommands', [])
const bg_enable = useStorage('bg_enable', true)
const bg_opacity = useStorage('bg_opacity', 0.95)
const hasPrefix = computed(() => command.value.startsWith('console') || command.value.startsWith('docker'))
const hasPlaceholder = computed(() => command.value && command.value.match(/\[.*\]/))
const show_page_tips = useStorage('show_page_tips', true)
@@ -273,9 +273,17 @@ onUnmounted(() => {
if (sse) {
sse.close()
}
if (bg_enable.value && bg_opacity.value) {
document.querySelector('body').setAttribute("style", `opacity: ${bg_opacity.value}`)
}
})
onMounted(async () => {
if (bg_enable.value) {
document.querySelector('body').setAttribute("style", "opacity: 1");
}
window.addEventListener("resize", reSizeTerminal);
command_input.value.focus()
@@ -289,6 +297,7 @@ onMounted(async () => {
cols: 108,
rows: 10,
disableStdin: true,
convertEol: true,
})
terminal.value.open(outputConsole.value)
terminal.value.loadAddon(terminalFit.value)

View File

@@ -8,6 +8,18 @@
</span>
<div class="is-pulled-right">
<div class="field is-grouped">
<div class="control has-icons-left" v-if="toggleFilter || query">
<input type="search" v-model.lazy="query" class="input" id="filter"
placeholder="Filter displayed content">
<span class="icon is-left"><i class="fas fa-filter"/></span>
</div>
<div class="control">
<button class="button is-danger is-light" @click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter"/></span>
</button>
</div>
<p class="control">
<button class="button is-primary" v-tooltip.bottom="'Add new variable'" @click="toggleForm = !toggleForm"
:disabled="isLoading">
@@ -31,12 +43,16 @@
</div>
</div>
<div class="column is-12" v-if="!toggleForm && items.length < 1">
<div class="column is-12" v-if="!toggleForm && filteredRows.length < 1">
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
<Message v-else message_class="has-background-warning-90 has-text-dark" title="Information"
<Message v-else message_class="has-background-warning-90 has-text-dark"
:title="query ? 'No results' : 'Information'"
icon="fas fa-info-circle">
<p>
<p v-if="query">
No environment variables found matching <strong>{{ query }}</strong>. Please try a different filter.
</p>
<p v-else>
No environment variables configured yet. Click on the
<i @click="toggleForm=true" class="is-clickable fa fa-add"></i> button to add a new variable.
</p>
@@ -116,9 +132,9 @@
</div>
</form>
</div>
<div v-else class="column is-12" v-if="items">
<div v-else class="column is-12" v-if="filteredRows">
<div class="columns is-multiline">
<div class="column" v-for="item in filteredRows(items)" :key="item.key"
<div class="column" v-for="item in filteredRows" :key="item.key"
:class="{'is-4':!item?.danger,'is-12':item.danger}">
<div class="card" :class="{ 'is-danger': item?.danger }">
<header class="card-header">
@@ -220,6 +236,8 @@ import {awaitElement, copyText, notification} from '~/utils/index'
import {useStorage} from '@vueuse/core'
import Message from '~/components/Message'
const route = useRoute()
useHead({title: 'Environment Variables'})
const items = ref([])
@@ -230,6 +248,13 @@ const form_type = ref()
const show_page_tips = useStorage('show_page_tips', true)
const isLoading = ref(true)
const file = ref('.env')
const query = ref(route.query.filter ?? '')
const toggleFilter = ref(false)
watch(toggleFilter, () => {
if (!toggleFilter.value) {
query.value = ''
}
});
const loadContent = async () => {
const route = useRoute()
@@ -400,7 +425,13 @@ const getHelp = key => {
const fixBool = v => [true, 'true', '1'].includes(v)
const filteredRows = rows => rows.filter(i => i.value !== undefined)
const filteredRows = computed(() => {
if (!query.value) {
return items.value.filter(i => i.value !== undefined)
}
return items.value.filter(i => i.key.toLowerCase().includes(query.value.toLowerCase())).filter(i => i.value !== undefined)
})
const stateCallBack = async e => {
if (!e.state && !e.detail) {

View File

@@ -9,8 +9,10 @@
<div class="is-pulled-right">
<div class="field is-grouped">
<div class="control has-icons-left" v-if="toggleFilter || query">
<input type="search" v-model.lazy="query" class="input" id="filter" placeholder="Filter">
<span class="icon is-left"><i class="fas fa-filter"/></span>
<form @submit.prevent="loadContent">
<input type="search" v-model="query" class="input" id="filter" placeholder="Search & Filter">
<span class="icon is-left"><i class="fas fa-filter"/></span>
</form>
</div>
<div class="control">
@@ -63,6 +65,12 @@
<header class="card-header is-align-self-flex-end">
<div class="card-header-title is-block">
<NuxtLink @click="quick_view = item.id" v-text="makeName(item.id)"/>
<span v-if="item?.delay_by" class="tag is-warning is-pulled-right is-hidden-mobile has-tooltip"
v-tooltip="'The event dispatching was delayed by this many seconds.'">
<span class="icon"><i class="fas fa-clock"/></span>
<span>{{ item.delay_by }}s</span>
</span>
<div class="is-pulled-right is-hidden-tablet">
<span class="tag" :class="getStatusClass(item.status)">{{ statuses[item.status].name }}</span>
</div>
@@ -133,6 +141,9 @@
<li>Resetting event will return it to the queue to be dispatched again.</li>
<li>Stopping event will prevent it from being dispatched.</li>
<li>Events with status of <span class="tag is-warning">Running</span> Cannot be cancelled or stopped.</li>
<li>The filter <i class="fa fa-filter"/> button on top can be used for both filtering the displayed
results, and on submit it will search the backend for the given event name.
</li>
</ul>
</Message>
</div>

View File

@@ -3,27 +3,28 @@
<div class="columns is-multiline">
<div class="column is-12 is-clearfix">
<span class="title is-4">
<span class="icon"><i class="fas fa-globe" /></span>
<span class="icon"><i class="fas fa-globe"/></span>
Logs
</span>
<div class="is-pulled-right">
<div class="field is-grouped">
<div class="control has-icons-left" v-if="toggleFilter">
<input type="search" v-model.lazy="query" class="input" id="filter" placeholder="Filter">
<span class="icon is-left"><i class="fas fa-filter" /></span>
<input type="search" v-model.lazy="query" class="input" id="filter"
placeholder="Filter displayed content">
<span class="icon is-left"><i class="fas fa-filter"/></span>
</div>
<div class="control">
<button class="button is-danger is-light" v-tooltip.bottom="'Filter files.'"
@click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter" /></span>
@click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter"/></span>
</button>
</div>
<p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading"
:class="{ 'is-loading': isLoading }">
<span class="icon"><i class="fas fa-sync" /></span>
:class="{ 'is-loading': isLoading }">
<span class="icon"><i class="fas fa-sync"/></span>
</button>
</p>
</div>
@@ -33,11 +34,16 @@
</div>
</div>
<div class="column is-12" v-if="logs.length < 1 || isLoading">
<div class="column is-12" v-if="filterItems.length < 1 || isLoading">
<Message v-if="isLoading" message_class="is-background-info-90 has-text-dark" icon="fas fa-spinner fa-spin"
title="Loading" message="Loading data. Please wait..." />
<Message v-else title="Warning" message_class="is-background-warning-80 has-text-dark"
icon="fas fa-exclamation-triangle" message="No logs files found." />
title="Loading" message="Loading data. Please wait..."/>
<Message v-else
:title="query ? 'No results' : 'Warning'"
message_class="is-background-warning-80 has-text-dark"
icon="fas fa-exclamation-triangle">
<span v-if="query">No results found for <strong>{{ query }}</strong></span>
<span v-else>No logs found.</span>
</Message>
</div>
<div class="column is-4-tablet" v-for="(item, index) in filterItems" :key="'log-' + index">
@@ -47,22 +53,22 @@
<NuxtLink :to="'/logs/' + item.filename">{{ item.filename ?? item.date }}</NuxtLink>
</p>
<span class="card-header-icon">
<span class="icon" v-if="'access' === item.type"><i class="fas fa-key" /></span>
<span class="icon" v-if="'task' === item.type"><i class="fas fa-tasks" /></span>
<span class="icon" v-if="'app' === item.type"><i class="fas fa-bugs" /></span>
<span class="icon" v-if="'webhook' === item.type"><i class="fas fa-book" /></span>
<span class="icon" v-if="'access' === item.type"><i class="fas fa-key"/></span>
<span class="icon" v-if="'task' === item.type"><i class="fas fa-tasks"/></span>
<span class="icon" v-if="'app' === item.type"><i class="fas fa-bugs"/></span>
<span class="icon" v-if="'webhook' === item.type"><i class="fas fa-book"/></span>
<span class="is-capitalized">{{ item.type }}</span>
</span>
</header>
<div class="card-footer">
<p class="card-footer-item">
<span class="icon"><i class="fas fa-calendar" />&nbsp;</span>
<span class="icon"><i class="fas fa-calendar"/>&nbsp;</span>
<span class="has-tooltip" v-tooltip="`Last Update: ${moment(item.modified).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment(item.modified).fromNow() }}
</span>
</p>
<p class="card-footer-item">
<span class="icon"><i class="fas fa-hdd" />&nbsp;</span>
<span class="icon"><i class="fas fa-hdd"/>&nbsp;</span>
<span>{{ humanFileSize(item.size) }}</span>
</p>
</div>
@@ -75,10 +81,10 @@
<script setup>
import request from '~/utils/request'
import moment from 'moment'
import { humanFileSize, TOOLTIP_DATE_FORMAT } from '~/utils/index'
import {humanFileSize, TOOLTIP_DATE_FORMAT} from '~/utils/index'
import Message from '~/components/Message'
useHead({ title: 'Logs' })
useHead({title: 'Logs'})
const query = ref()
const logs = ref([])

View File

@@ -109,6 +109,7 @@ final class Command
'LANG' => 'en_US.UTF-8',
'LC_ALL' => 'en_US.UTF-8',
'TERM' => 'xterm-256color',
'FORCE_COLOR' => (string)$data->get('force_color', 'true'),
'PWD' => $path,
], $_ENV),
timeout: $data->get('timeout', 7200),

View File

@@ -32,10 +32,15 @@ final readonly class Events
#[Get(pattern: self::URL . '[/]')]
public function list(iRequest $request): iResponse
{
$params = DataUtil::fromRequest($request, true);
[$page, $perpage, $start] = getPagination($request, 1, self::PERPAGE);
$arrParams = [];
if (null !== ($filter = $params->get('filter'))) {
$arrParams['event'] = [DBLayer::IS_LIKE, $filter];
}
$this->repo->setPerpage($perpage)->setStart($start)->setDescendingOrder();
$entities = $this->repo->findAll($arrParams, [
@@ -194,6 +199,10 @@ final readonly class Events
$data = $entity->getAll();
$data['status_name'] = $entity->getStatusText();
if ($delay = ag($entity->options, Options::DELAY_BY)) {
$data['delay_by'] = $delay;
}
return $data;
}
}

View File

@@ -254,14 +254,6 @@ class BackupCommand extends Command
continue;
}
if (true !== (bool)ag($backend, 'import.enabled')) {
$this->logger->info("SYSTEM: Ignoring '{user}@{backend}'. Import disabled.", [
'user' => $userContext->name,
'backend' => $backendName
]);
continue;
}
if (!isset($supported[$type])) {
$this->logger->error("SYSTEM: Ignoring '{user}@{backend}'. Unexpected type '{type}'.", [
'user' => $userContext->name,
@@ -272,6 +264,24 @@ class BackupCommand extends Command
continue;
}
if (true !== (bool)ag($backend, 'import.enabled')) {
if ($isCustom) {
$this->logger->notice(
"SYSTEM: The backend '{user}@{backend}' has import disabled, However the check is skipped due to --select-backend.",
[
'user' => $userContext->name,
'backend' => $backendName
]
);
} else {
$this->logger->info("SYSTEM: Ignoring '{user}@{backend}'. Import disabled.", [
'user' => $userContext->name,
'backend' => $backendName
]);
continue;
}
}
if (null === ($url = ag($backend, 'url')) || false === isValidURL($url)) {
$this->logger->error("SYSTEM: Ignoring '{user}@{backend}'. Invalid URL '{url}'.", [
'user' => $userContext->name,