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:
@@ -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;
|
||||
})();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 </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}})
|
||||
|
||||
@@ -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')]
|
||||
|
||||
Reference in New Issue
Block a user