Updated ENV web interface as well as the env API endpoint. Did some Facelift sn the Tasks page to be more mobile friendly.

This commit is contained in:
Abdulmhsen B. A. A
2024-05-08 13:38:37 +03:00
parent 194fe94128
commit ccb00dfda1
6 changed files with 188 additions and 109 deletions

View File

@@ -8,139 +8,168 @@
*/
return (function () {
// -- Do not forget to update the tasks list if you add a new task.
$tasks = ['import', 'export', 'push', 'progress', 'backup', 'prune', 'indexes', 'requests'];
$task_env = [
'WS_CRON_{task}' => [
'desc' => 'Enable the {task} task.',
'type' => 'bool',
],
'WS_CRON_{task}_AT' => [
'desc' => 'The time to run the {task} task.',
'type' => 'string',
],
'WS_CRON_{task}_ARGS' => [
'desc' => 'The arguments to pass to the {task} task.',
'type' => 'string',
],
];
$env = [];
foreach ($tasks as $task) {
foreach ($task_env as $key => $info) {
$info['desc'] = r($info['desc'], ['task' => $task]);
$env[r($key, ['task' => strtoupper($task)])] = $info;
}
}
$env = array_replace_recursive($env, [
'WS_DATA_PATH' => [
$env = [
[
'key' => 'WS_DATA_PATH',
'description' => 'Where to store main data. (config, db).',
'type' => 'string',
],
'WS_TMP_DIR' => [
[
'key' => 'WS_TMP_DIR',
'description' => 'Where to store temp data. (logs, cache)',
'type' => 'string',
],
'WS_TZ' => [
[
'key' => 'WS_TZ',
'description' => 'Set the Tool timezone.',
'type' => 'string',
],
'WS_LOGS_CONTEXT' => [
[
'key' => 'WS_LOGS_CONTEXT',
'description' => 'Enable context in logs.',
'type' => 'bool',
],
'WS_LOGGER_FILE_ENABLE' => [
[
'key' => 'WS_LOGGER_FILE_ENABLE',
'description' => 'Enable logging to app.log file',
'type' => 'bool',
],
'WS_LOGGER_FILE_LEVEL' => [
[
'key' => 'WS_LOGGER_FILE_LEVEL',
'description' => 'Set the log level for the file logger. Default: ERROR',
'type' => 'string',
],
'WS_WEBHOOK_DUMP_REQUEST' => [
[
'key' => 'WS_WEBHOOK_DUMP_REQUEST',
'description' => 'Dump all requests to webhook endpoint to a json file.',
'type' => 'bool',
],
'WS_TRUST_PROXY' => [
[
'key' => 'WS_TRUST_PROXY',
'description' => 'Trust the IP from the WS_TRUST_HEADER header.',
'type' => 'bool',
],
'WS_TRUST_HEADER' => [
[
'key' => 'WS_TRUST_HEADER',
'description' => 'The header with the true user IP.',
'type' => 'string',
],
'WS_LIBRARY_SEGMENT' => [
[
'key' => 'WS_LIBRARY_SEGMENT',
'description' => 'How many items to request per a request.',
'type' => 'string',
],
'WS_CACHE_URL' => [
[
'key' => 'WS_CACHE_URL',
'description' => 'The URL to the cache server.',
'type' => 'string',
'mask' => true,
],
'WS_CACHE_NULL' => [
[
'key' => 'WS_CACHE_NULL',
'description' => 'Enable the null cache. This is useful for testing. Or first time container startup.',
'type' => 'bool',
],
'WS_WEBUI_ENABLED' => [
[
'key' => 'WS_WEBUI_ENABLED',
'description' => 'Enable the web UI.',
'type' => 'bool',
],
'WS_API_KEY' => [
[
'key' => 'WS_API_KEY',
'description' => 'The API key to allow access to the API',
'type' => 'string',
'mask' => true,
],
'WS_LOGS_PRUNE_AFTER' => [
[
'key' => 'WS_LOGS_PRUNE_AFTER',
'description' => 'Prune logs after this many days.',
'type' => 'int',
],
'WS_EXPORT_THRESHOLD' => [
[
'key' => 'WS_EXPORT_THRESHOLD',
'description' => 'Trigger full export mode if changes exceed this number.',
'type' => 'int',
],
'WS_EPISODES_DISABLE_GUID' => [
[
'key' => 'WS_EPISODES_DISABLE_GUID',
'description' => 'Disable the GUID field in the episodes.',
'type' => 'bool',
'deprecated' => true,
],
'WS_BACKENDS_FILE' => [
[
'key' => 'WS_BACKENDS_FILE',
'description' => 'The full path to the backends file.',
'type' => 'string',
],
'WS_WEBHOOK_LOG_FILE_FORMAT' => [
[
'key' => 'WS_WEBHOOK_LOG_FILE_FORMAT',
'description' => 'The name format for the webhook log file. Anything inside {} will be replaced with data from the webhook payload.',
'type' => 'string',
],
'WS_CACHE_PREFIX' => [
[
'key' => 'WS_CACHE_PREFIX',
'description' => 'The prefix for the cache keys.',
'type' => 'string',
],
'WS_CACHE_PATH' => [
[
'key' => 'WS_CACHE_PATH',
'description' => 'The path to the cache directory. This is usually if the cache server is not available.',
'type' => 'string',
],
'WS_LOGGER_SYSLOG_FACILITY' => [
[
'key' => 'WS_LOGGER_SYSLOG_FACILITY',
'description' => 'The syslog facility to use.',
'type' => 'string',
],
'WS_LOGGER_SYSLOG_ENABLED' => [
[
'key' => 'WS_LOGGER_SYSLOG_ENABLED',
'description' => 'Enable logging to syslog.',
'type' => 'bool',
],
'WS_LOGGER_SYSLOG_LEVEL' => [
[
'key' => 'WS_LOGGER_SYSLOG_LEVEL',
'description' => 'Set the log level for the syslog logger. Default: ERROR',
'type' => 'string',
],
'WS_SECURE_API_ENDPOINTS' => [
[
'key' => 'WS_SECURE_API_ENDPOINTS',
'description' => 'Disregard the open route policy, and require an API key for all routes.',
'type' => 'bool',
],
]);
];
ksort($env);
// -- Do not forget to update the tasks list if you add a new task.
$tasks = ['import', 'export', 'push', 'progress', 'backup', 'prune', 'indexes', 'requests'];
$task_env = [
[
'key' => 'WS_CRON_{task}',
'description' => 'Enable the {task} task.',
'type' => 'bool',
],
[
'key' => 'WS_CRON_{task}_AT',
'description' => 'The time to run the {task} task.',
'type' => 'string',
],
[
'key' => 'WS_CRON_{task}_ARGS',
'description' => 'The arguments to pass to the {task} task.',
'type' => 'string',
],
];
foreach ($tasks as $task) {
foreach ($task_env as $info) {
$info['key'] = r($info['key'], ['task' => strtoupper($task)]);
$info['description'] = r($info['description'], ['task' => $task]);
$env[] = $info;
}
}
// -- sort based on the array name key
$sorter = array_column($env, 'key');
array_multisort($sorter, SORT_ASC, $env);
return $env;
})();

View File

@@ -162,3 +162,24 @@ hr {
.vue-notification-wrapper {
padding-top: 0.5em;
}
.card.is-danger {
border: var(--bulma-control-border-width) solid var(--bulma-danger);
}
.card.is-info {
border: var(--bulma-control-border-width) solid var(--bulma-info);
}
.card.is-success {
border: var(--bulma-control-border-width) solid var(--bulma-success);
}
.card.is-warning {
border: var(--bulma-control-border-width) solid var(--bulma-warning);
}
.card.is-gray {
border: var(--bulma-control-border-width) solid rgba(56, 56, 56, 0.38);
}

View File

@@ -2,7 +2,7 @@
<div class="container">
<nav class="navbar is-dark mb-4">
<div class="navbar-brand pl-5">
<NuxtLink class="navbar-item" href="/">
<NuxtLink class="navbar-item" href="/" @click.native="showMenu=false">
<span class="icon-text">
<span class="icon"><i class="fas fa-home"></i></span>
<span>Home</span>
@@ -18,49 +18,49 @@
<div class="navbar-menu" :class="{'is-active':showMenu}">
<div class="navbar-start">
<NuxtLink class="navbar-item" href="/backends">
<NuxtLink class="navbar-item" href="/backends" @click.native="showMenu=false">
<span class="icon-text">
<span class="icon"><i class="fas fa-server"></i></span>
<span>Backends</span>
</span>
</NuxtLink>
<NuxtLink class="navbar-item" href="/history">
<NuxtLink class="navbar-item" href="/history" @click.native="showMenu=false">
<span class="icon-text">
<span class="icon"><i class="fas fa-history"></i></span>
<span>History</span>
</span>
</NuxtLink>
<NuxtLink class="navbar-item" href="/tasks">
<NuxtLink class="navbar-item" href="/tasks" @click.native="showMenu=false">
<span class="icon-text">
<span class="icon"><i class="fas fa-tasks"></i></span>
<span>Tasks</span>
</span>
</NuxtLink>
<NuxtLink class="navbar-item" href="/env">
<NuxtLink class="navbar-item" href="/env" @click.native="showMenu=false">
<span class="icon-text">
<span class="icon"><i class="fas fa-cogs"></i></span>
<span>Env</span>
</span>
</NuxtLink>
<NuxtLink class="navbar-item" href="/logs">
<NuxtLink class="navbar-item" href="/logs" @click.native="showMenu=false">
<span class="icon-text">
<span class="icon"><i class="fas fa-globe"></i></span>
<span>Logs</span>
</span>
</NuxtLink>
<NuxtLink class="navbar-item" href="/console">
<NuxtLink class="navbar-item" href="/console" @click.native="showMenu=false">
<span class="icon-text">
<span class="icon"><i class="fas fa-terminal"></i></span>
<span>Console</span>
</span>
</NuxtLink>
<NuxtLink class="navbar-item" href="/report">
<NuxtLink class="navbar-item" href="/report" @click.native="showMenu=false">
<span class="icon-text">
<span class="icon"><i class="fas fa-flag"></i></span>
<span>S. Report</span>

View File

@@ -31,15 +31,27 @@
<div class="column is-12" v-if="toggleForm">
<form id="env_add_form" @submit.prevent="addVariable">
<div class="field is-grouped">
<div class="control is-expanded">
<input class="input" type="text" placeholder="Key" v-model="form_key">
<p class="help has-text-danger" v-if="form_key && !form_key.toLowerCase().startsWith('ws_')">
Key Must start with WS_
</p>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="form_key" id="form_key" @change="keyChanged">
<option value="" disabled>Select Key</option>
<option v-for="env in envs" :key="env.key" :value="env.key">
{{ env.key }}
</option>
</select>
</div>
<div class="icon is-small is-left">
<i class="fas fa-key"></i>
</div>
</div>
<div class="control is-expanded">
<input class="input" type="text" placeholder="Value" v-model="form_value">
<div class="control is-expanded has-icons-left">
<input class="input" id="form_value" type="text" placeholder="Value" v-model="form_value">
<div class="icon is-small is-left">
<i class="fas fa-font"></i>
</div>
<p class="help" v-html="getHelp(form_key)"></p>
</div>
<div class="control">
<button class="button is-danger" type="button"
v-tooltip="'Cancel'" @click="form_key=null; form_value=null; toggleForm=false">
@@ -55,7 +67,7 @@
</form>
</div>
<div class="column is-12">
<div class="column is-12" v-if="envs">
<div class="table-container">
<table class="table is-fullwidth is-bordered is-striped is-hoverable has-text-centered">
<thead>
@@ -66,7 +78,7 @@
</tr>
</thead>
<tbody>
<tr v-for="env in envs" :key="env.key">
<tr v-for="env in filteredRows(envs)" :key="env.key">
<td class="has-text-left">
{{ env.key }}
<div class="is-pulled-right" v-if="env.mask">
@@ -115,14 +127,14 @@
</template>
<script setup>
import request from "~/utils/request.js";
import {awaitElement, notification} from "~/utils/index.js";
import request from '~/utils/request.js'
import {awaitElement, notification} from '~/utils/index.js'
useHead({title: 'Environment Variables'})
const envs = ref([])
const toggleForm = ref(false)
const form_key = ref(null)
const form_key = ref('')
const form_value = ref(null)
const file = ref('.env')
const copyAPI = navigator.clipboard
@@ -192,4 +204,33 @@ watch(toggleForm, (value) => {
}))
}
});
const keyChanged = () => {
if (!form_key.value) {
return
}
let data = envs.value.filter(i => i.key === form_key.value)
form_value.value = (data.length > 0) ? data[0].value : ''
}
const getHelp = (key) => {
if (!key) {
return ''
}
let data = envs.value.filter(i => i.key === key)
if (data.length === 0) {
return ''
}
let text = `${data[0].description}`;
if (data[0].type) {
text += ` Expected value: <code>${('bool' === data[0].type) ? 'bool, 0, 1' : data[0].type}</code>`
}
return text;
}
const filteredRows = (rows) => rows.filter(i => i.value !== undefined);
</script>

View File

@@ -16,20 +16,22 @@
</div>
</div>
</div>
<div class="is-hidden-mobile" v-if="queued.length > 0">
<p>
<span>The following tasks <code>{{ queued.join(', ') }}</code> are queued to be run soon.</span>
</p>
<div class="subtitle is-hidden-mobile" v-if="queued.length > 0">
<p>The following tasks <code>{{ queued.join(', ') }}</code> are queued to be run in background soon.</p>
</div>
</div>
<div v-for="task in tasks" :key="task.name" class="column is-6-tablet is-12-mobile">
<div class="card">
<div class="card" :class="{ 'is-gray' : !task.enabled, 'is-success': task.enabled }">
<header class="card-header">
<div class="is-capitalized card-header-title is-centered has-tooltip"
v-tooltip="'The command: ' + task.command">
<div class="is-capitalized card-header-title">
{{ task.name }}
</div>
<span class="card-header-icon" v-tooltip="'Enable/Disable Task.'">
<input :id="task.name" type="checkbox" class="switch is-success" :checked="task.enabled"
@change="toggleTask(task)">
<label :for="task.name"></label>
</span>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
@@ -71,20 +73,13 @@
</div>
<footer class="card-footer">
<div class="card-footer-item">
<div class="field">
<input :id="task.name" type="checkbox" class="switch is-success" :checked="task.enabled"
@change="toggleTask(task)">
<label :for="task.name">
<span class="is-hidden-mobile">Task is&nbsp;</span> {{ task.enabled ? 'Enabled' : 'Disabled' }}
</label>
</div>
</div>
<div class="card-footer-item" v-if="task.enabled">
<button class="button is-info" @click="queueTask(task)" :disabled="task.queued">
<button class="button is-info" @click="queueTask(task)" :disabled="task.queued || !task.enabled">
<span class="icon-text">
<span class="icon"><i class="fas fa-clock"></i></span>
<span v-if="!task.queued">Queue</span>
<span v-else>Queued</span>
<span>
<template v-if="!task.queued">Queue Task</template>
<template v-else>Queued</template>
</span>
</span>
</button>
</div>
@@ -138,7 +133,7 @@ const toggleTask = async (task) => {
}
const queueTask = async (task) => {
if (!confirm(`Are you sure you want to queue the task ${task.name}?`)) {
if (!confirm(`Queue '${task.name}' to run in background?`)) {
return
}
@@ -150,7 +145,7 @@ const queueTask = async (task) => {
}
const confirmRun = async (task) => {
if (!confirm(`Are you sure you want to run ${task.name}?`)) {
if (!confirm(`Are you sure you want to run '${task.name}' via web console now?`)) {
return
}
await navigateTo({path: '/console', query: {task: task.name, keep: 1}})

View File

@@ -29,24 +29,17 @@ final class Env
{
$spec = require __DIR__ . '/../../../config/env.spec.php';
$response = [
'data' => [],
'file' => Config::get('path') . '/config/.env',
];
foreach ($this->envFile->getAll() as $key => $val) {
if (false === str_starts_with($key, 'WS_')) {
foreach ($spec as &$info) {
if (!$this->envFile->has($info['key'])) {
continue;
}
$response['data'][] = [
'key' => $key,
'value' => $val,
'mask' => (bool)ag($spec, "{$key}.mask", false),
];
$info['value'] = $this->envFile->get($info['key']);
}
return api_response(HTTP_STATUS::HTTP_OK, $response);
return api_response(HTTP_STATUS::HTTP_OK, [
'data' => $spec,
'file' => Config::get('path') . '/config/.env',
]);
}
#[Get(self::URL . '/{key}[/]', name: 'system.env.view')]