Finalizing The WebUI for alpha/beta testing next week.
The WebUI is mostly done for now, The only missing piece is backend view, which isn't something we will include in the first release.
This commit is contained in:
@@ -183,3 +183,32 @@ hr {
|
||||
border: var(--bulma-control-border-width) solid rgba(56, 56, 56, 0.38);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px), print {
|
||||
|
||||
.field.is-grouped-tablet {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.field.is-grouped-tablet > .control {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.field.is-grouped-tablet > .control.is-expanded {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.field.is-grouped-tablet.is-grouped-centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.field.is-grouped-tablet.is-grouped-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.field.is-grouped-tablet.is-grouped-multiline {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
@@ -25,7 +25,7 @@
|
||||
<label class="label">Name</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.name" required>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
@@ -42,13 +42,15 @@
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.token" required>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
<template v-if="'plex'===backend.type">
|
||||
Enter the <code>X-Plex-Token</code>.<a target="_blank" href="https://support.plex.tv/articles/204059436">
|
||||
Visit This article for more information</a>.
|
||||
Enter the <code>X-Plex-Token</code>.
|
||||
<NuxtLink target="_blank" href="https://support.plex.tv/articles/204059436">
|
||||
Visit This article for more information.
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-else>
|
||||
Generate a new API token from <code>Dashboard > Settings > API Keys</code>.
|
||||
@@ -68,7 +70,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<input class="input" type="text" v-model="backend.url" v-else required>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-link" v-if="!serversLoading"></i>
|
||||
<i class="fas fa-spinner fa-pulse" v-else></i>
|
||||
|
||||
@@ -86,7 +88,7 @@
|
||||
<label class="label">URL</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.url" required>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-link"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
@@ -100,7 +102,7 @@
|
||||
<label class="label">Unique Identifier</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.uuid" required>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-server" v-if="!uuidLoading"></i>
|
||||
<i class="fas fa-spinner fa-pulse" v-else></i>
|
||||
</div>
|
||||
@@ -125,7 +127,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<input class="input" type="text" v-model="backend.user" v-else>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-user-tie" v-if="!usersLoading"></i>
|
||||
<i class="fas fa-spinner fa-pulse" v-else></i>
|
||||
</div>
|
||||
@@ -188,7 +190,7 @@
|
||||
<div class="field has-text-right">
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">
|
||||
<span class="icon is-small"><i class="fas fa-plus"></i></span>
|
||||
<span class="icon"><i class="fas fa-plus"></i></span>
|
||||
<span>Add Backend</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -213,7 +215,7 @@ const props = defineProps({
|
||||
|
||||
const backend = ref({
|
||||
name: '',
|
||||
type: '',
|
||||
type: 'plex',
|
||||
url: '',
|
||||
token: '',
|
||||
uuid: '',
|
||||
|
||||
@@ -72,22 +72,16 @@
|
||||
<div class="navbar-item">
|
||||
<button class="button is-dark" @click="selectedTheme = 'light'" v-if="'dark' === selectedTheme"
|
||||
v-tooltip="'Switch to light theme'">
|
||||
<span class="icon is-small has-text-warning">
|
||||
<i class="fas fa-sun"></i>
|
||||
</span>
|
||||
<span class="icon has-text-warning"><i class="fas fa-sun"></i></span>
|
||||
</button>
|
||||
<button class="button is-dark" @click="selectedTheme = 'dark'" v-if="'light' === selectedTheme"
|
||||
v-tooltip="'Switch to dark theme'">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-moon"></i>
|
||||
</span>
|
||||
<span class="icon"><i class="fas fa-moon"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<button class="button is-dark" @click="showConnection = !showConnection" v-tooltip="'Configure connection'">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-cog"></i>
|
||||
</span>
|
||||
<span class="icon"><i class="fas fa-cog"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,7 +114,10 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">Can be obtained by using the <code>system:apikey</code> command.</p>
|
||||
<p class="help">
|
||||
You can obtain the <code>API TOKEN</code> by using the <code>system:apikey</code> command or by
|
||||
viewing the <code>/config/config/.env</code> and looking for the <code>WS_API_KEY=</code> key.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,14 +157,14 @@
|
||||
placeholder="API Path... /v1/api"
|
||||
@keyup="api_status = false; api_response = ''">
|
||||
<p class="help">
|
||||
Use <a href="javascript:void(0)" @click="api_path = '/v1/api'">Set default API</a>.
|
||||
Use <a href="javascript:void(0)" @click="api_path = '/v1/api'">Set default API Path</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped has-addons-right">
|
||||
<div class="field">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="field has-addons">
|
||||
@@ -184,7 +181,13 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">These settings are stored locally in your browser.</p>
|
||||
<p class="help">
|
||||
<span class="icon-text">
|
||||
<span class="icon has-text-danger"><i class="fas fa-info"></i></span>
|
||||
<span>These settings are stored locally in your browser. You need to re-add them if you access the
|
||||
<code>WebUI</code> from different browser.</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,30 +203,40 @@
|
||||
</template>
|
||||
|
||||
<div class="columns is-multiline mt-3">
|
||||
<div class="column is-12">
|
||||
<div class="column is-12 is-hidden-mobile">
|
||||
<div class="content">
|
||||
If you have question, want clarification on something, or just want to chat with other users, you are welcome
|
||||
to join our <a href="https://discord.gg/haUXHJyj6Y" rel="noreferrer,nofollow,noopener" target="_blank">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-brands fa-discord"></i></span>
|
||||
<span>Discord server</span>
|
||||
</span>
|
||||
</a>. For real bug reports, feature requests, or contributions, please visit the <a
|
||||
href="https://github.com/arabcoders/watchstate/issues/new/choose" rel="noreferrer,nofollow,noopener">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-brands fa-github"></i></span>
|
||||
<span>GitHub repository</span>
|
||||
</span>
|
||||
</a>.
|
||||
<Message v-if="show_page_info" title="Information">
|
||||
<button class="delete" @click="show_page_info = false"></button>
|
||||
If you have question, or want clarification on something, or just want to chat with other users, you are
|
||||
welcome to join our
|
||||
<NuxtLink href="https://discord.gg/haUXHJyj6Y" target="_blank">
|
||||
<span class="icon-text is-underlined">
|
||||
<span class="icon"><i class="fas fa-brands fa-discord"></i></span>
|
||||
<span>Discord server</span>
|
||||
</span>
|
||||
</NuxtLink>
|
||||
. For real bug reports, feature requests, or contributions, please visit the
|
||||
<NuxtLink href="https://github.com/arabcoders/watchstate/issues/new/choose" target="_blank">
|
||||
<span class="icon-text is-underlined">
|
||||
<span class="icon"><i class="fas fa-brands fa-github"></i></span>
|
||||
<span>GitHub repository</span>
|
||||
</span>
|
||||
</NuxtLink>
|
||||
.
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6 is-12-mobile has-text-left">
|
||||
{{ api_version }} - <a href="https://github.com/arabcoders/watchstate" target="_blank">WatchState</a>
|
||||
<template v-if="!show_page_info">
|
||||
<span class="is-hidden-mobile">
|
||||
- <a href="javascript:void(0)" @click="show_page_info=true">Show Info</a>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NuxtNotifications position="top right" :speed="800" :ignoreDuplicates="true" :width="340" :pauseOnHover="true"/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -241,15 +254,15 @@ const showConnection = ref(false)
|
||||
const api_url = useStorage('api_url', window.location.origin)
|
||||
const api_path = useStorage('api_path', '/v1/api')
|
||||
const api_token = useStorage('api_token', '')
|
||||
const show_page_info = useStorage('show_page_info', true)
|
||||
const api_status = ref(false)
|
||||
const api_response = ref('Status: Unknown')
|
||||
const api_version = useStorage('api_version', 'dev-master')
|
||||
|
||||
const Year = ref(new Date().getFullYear())
|
||||
const showMenu = ref(false)
|
||||
const exposeToken = ref(false)
|
||||
|
||||
const applyPreferredColorScheme = (scheme) => {
|
||||
const applyPreferredColorScheme = scheme => {
|
||||
for (let s = 0; s < document.styleSheets.length; s++) {
|
||||
for (let i = 0; i < document.styleSheets[s].cssRules.length; i++) {
|
||||
try {
|
||||
@@ -298,7 +311,7 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedTheme, (value) => {
|
||||
watch(selectedTheme, value => {
|
||||
try {
|
||||
applyPreferredColorScheme(value)
|
||||
} catch (e) {
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@vueuse/nuxt": "^10.9.0",
|
||||
"cronstrue": "^2.49.0",
|
||||
"floating-vue": "^5.2.2",
|
||||
"moment": "^2.30.1",
|
||||
"nuxt": "^3.11.2",
|
||||
"nuxt3-notifications": "^1.2.0",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"cronstrue": "^2.49.0"
|
||||
}
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<label class="label">Name</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.name" required readonly disabled>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
@@ -40,7 +40,7 @@
|
||||
<label class="label">Type</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.type" readonly disabled>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-globe"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,7 +50,7 @@
|
||||
<label class="label">URL</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.url" required>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-link"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
@@ -67,7 +67,7 @@
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.token" required>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
@@ -88,7 +88,7 @@
|
||||
<label class="label">Backend Unique ID</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.uuid" required>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-server" v-if="!uuidLoading"></i>
|
||||
<i class="fas fa-spinner fa-pulse" v-else></i>
|
||||
</div>
|
||||
@@ -110,7 +110,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<input class="input" type="text" v-model="backend.user" v-else>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-user-tie" v-if="!usersLoading"></i>
|
||||
<i class="fas fa-spinner fa-pulse" v-else></i>
|
||||
</div>
|
||||
@@ -170,13 +170,13 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" @click="showOptions = !showOptions">
|
||||
<label class="label is-clickable" @click="showOptions = !showOptions">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i v-if="showOptions" class="fas fa-arrow-up"></i>
|
||||
<i v-else class="fas fa-arrow-down"></i>
|
||||
</span>
|
||||
<span>Optional options</span>
|
||||
<span>Additional options...</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="columns is-multiline is-mobile" v-if="showOptions && backend.options">
|
||||
@@ -189,7 +189,7 @@
|
||||
</div>
|
||||
<div class="column is-1">
|
||||
<button class="button is-danger" @click.prevent="removeOption(key)">
|
||||
<span class="icon is-small">
|
||||
<span class="icon">
|
||||
<i class="fas fa-trash"></i>
|
||||
</span>
|
||||
</button>
|
||||
@@ -201,7 +201,7 @@
|
||||
<div class="field has-text-right">
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">
|
||||
<span class="icon is-small">
|
||||
<span class="icon">
|
||||
<i class="fas fa-save"></i>
|
||||
</span>
|
||||
<span>Save Settings</span>
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
This page is not yet implemented. It will be used to display the details of a specific backend.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const backend = useRoute().params.backend
|
||||
|
||||
useHead({title: `Backends: ${backend}`})
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-primary" @click.prevent="loadContent">
|
||||
<span class="icon is-small">
|
||||
<span class="icon">
|
||||
<i class="fas fa-sync"></i>
|
||||
</span>
|
||||
</button>
|
||||
@@ -24,6 +24,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">This page contains all the backends that are currently configured.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="toggleForm">
|
||||
@@ -33,8 +36,8 @@
|
||||
<div v-for="backend in backends" :key="backend.name" class="column is-6-tablet is-12-mobile">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-centered is-word-break">
|
||||
<NuxtLink :href="'/backend/' + backend.name">
|
||||
<p class="card-header-title">
|
||||
<NuxtLink :href="`/backend/${backend.name}`">
|
||||
{{ backend.name }}
|
||||
</NuxtLink>
|
||||
</p>
|
||||
@@ -45,12 +48,12 @@
|
||||
</span>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="columns is-multiline is-mobile has-text-centered">
|
||||
<div class="column is-6-mobile" v-if="backend.export.enabled">
|
||||
<div class="columns is-multiline has-text-centered">
|
||||
<div class="column is-6 has-text-left-mobile" v-if="backend.export.enabled">
|
||||
<strong>Last Export:</strong>
|
||||
{{ backend.export.lastSync ? moment(backend.export.lastSync).fromNow() : 'None' }}
|
||||
</div>
|
||||
<div class="column is-hidden-mobile" v-if="backend.import.enabled">
|
||||
<div class="column is-6 has-text-left-mobile" v-if="backend.import.enabled">
|
||||
<strong>Last Import:</strong>
|
||||
{{ backend.import.lastSync ? moment(backend.import.lastSync).fromNow() : 'None' }}
|
||||
</div>
|
||||
@@ -63,7 +66,7 @@
|
||||
:checked="backend.export.enabled"
|
||||
@change="updateValue(backend, 'export.enabled', !backend.export.enabled)">
|
||||
<label :for="backend.name+'_export'">
|
||||
Export {{ backend.export.enabled ? 'Enabled' : 'Disabled' }}
|
||||
Export <span class="is-hidden-mobile"> {{ backend.export.enabled ? 'Enabled' : 'Disabled' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,7 +76,7 @@
|
||||
:checked="backend.import.enabled"
|
||||
@change="updateValue(backend, 'import.enabled',!backend.import.enabled)">
|
||||
<label :for="backend.name+'_import'">
|
||||
Import {{ backend.import.enabled ? 'Enabled' : 'Disabled' }}
|
||||
Import <span class="is-hidden-mobile"> {{ backend.import.enabled ? 'Enabled' : 'Disabled' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,6 +91,7 @@ import 'assets/css/bulma-switch.css'
|
||||
import moment from 'moment'
|
||||
import request from '~/utils/request.js'
|
||||
import BackendAdd from '~/components/BackendAdd.vue'
|
||||
import {notification} from '~/utils/index.js'
|
||||
|
||||
useHead({title: 'Backends'})
|
||||
|
||||
|
||||
@@ -14,37 +14,51 @@
|
||||
|
||||
<div class="column is-12">
|
||||
<form @submit.prevent="RunCommand">
|
||||
<div class="field is-grouped">
|
||||
<p class="control is-expanded has-icons-left">
|
||||
<input type="text" class="input" v-model="command" placeholder="system:view" autocomplete="off" autofocus
|
||||
:disabled="isLoading">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-terminal"></i>
|
||||
</span>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-primary" type="submit" :disabled="isLoading" :class="{'is-loading':isLoading}">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa fa-server"></i>
|
||||
<div class="field">
|
||||
<div class="field-body">
|
||||
<div class="field is-grouped-tablet">
|
||||
<p class="control is-expanded has-icons-left">
|
||||
<input type="text" class="input" v-model="command" placeholder="system:view" autocomplete="off"
|
||||
autofocus
|
||||
:disabled="isLoading">
|
||||
<span class="icon is-left">
|
||||
<i class="fas fa-terminal"></i>
|
||||
</span>
|
||||
<span>Run</span>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-info" type="button" v-tooltip="'Clear output'" @click="response = []">
|
||||
<span class="icon">
|
||||
<i class="fa fa-broom"></i>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control" v-if="isLoading">
|
||||
<button class="button is-danger" type="button" @click="finished" v-tooltip="'Close connection.'">
|
||||
<span class="icon">
|
||||
<i class="fa fa-power-off"></i>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-primary" type="submit" :disabled="isLoading" :class="{'is-loading':isLoading}">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa fa-server"></i>
|
||||
</span>
|
||||
<span>Run</span>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-info" type="button" v-tooltip="'Clear output'" @click="response = []">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa fa-broom"></i></span>
|
||||
<span>Clear</span>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control" v-if="isLoading">
|
||||
<button class="button is-danger" type="button" @click="finished" v-tooltip="'Close connection.'">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa fa-power-off"></i></span>
|
||||
<span>Close Connection</span>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa fa-info"></i></span>
|
||||
<span>Please beware, clicking close connection does not stop the command. It only stops the output from
|
||||
being displayed. The command will continue to run until it finishes.</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<div class="column is-12" v-if="toggleForm">
|
||||
<form id="env_add_form" @submit.prevent="addVariable">
|
||||
<div class="field is-grouped">
|
||||
<div class="field is-grouped-tablet">
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="form_key" id="form_key" @change="keyChanged">
|
||||
@@ -40,7 +40,7 @@
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-small is-left">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,17 +20,20 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">This page has the latest history entries. Sorted by the most recent event.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="total && last_page > 1">
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button rel="first" class="button" v-if="page !== 1" @click="loadContent(1)">
|
||||
<div class="control" v-if="page !== 1">
|
||||
<button rel="first" class="button" @click="loadContent(1)">
|
||||
<span><<</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button rel="prev" class="button" v-if="page > 1 && (page-1) !== 1" @click="loadContent(page-1)">
|
||||
<div class="control" v-if="page > 1 && (page-1) !== 1">
|
||||
<button rel="prev" class="button" @click="loadContent(page-1)">
|
||||
<span><</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -43,14 +46,13 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button rel="next" class="button" v-if="page !== last_page && (page+1) !== last_page"
|
||||
@click="loadContent(page+1)">
|
||||
<div class="control" v-if="page !== last_page && (page+1) !== last_page">
|
||||
<button rel="next" class="button" @click="loadContent(page+1)">
|
||||
<span>></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button rel="last" class="button" v-if="page !== last_page" @click="loadContent(last_page)">
|
||||
<div class="control" v-if="page !== last_page">
|
||||
<button rel="last" class="button" @click="loadContent(last_page)">
|
||||
<span>>></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -59,45 +61,53 @@
|
||||
|
||||
<div class="column is-12" v-if="searchForm">
|
||||
<form @submit.prevent="loadContent(1)">
|
||||
<div class="field has-addons">
|
||||
<div class="control has-icons-left">
|
||||
<div class="select">
|
||||
<select v-model="searchField" class="is-capitalized">
|
||||
<option value="">Select Field</option>
|
||||
<option v-for="field in searchable" :key="field" :value="field">
|
||||
{{ field }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-folder-tree"></i>
|
||||
<div class="field">
|
||||
<div class="field-body">
|
||||
<div class="field is-grouped-tablet">
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="searchField" class="is-capitalized" :disabled="isLoading">
|
||||
<option value="">Select Field</option>
|
||||
<option v-for="field in searchable" :key="'search-' + field.key" :value="field.key">
|
||||
{{ field.key }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-folder-tree"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control is-expanded has-icons-left">
|
||||
<input class="input" type="search" placeholder="Search..." v-model="query"
|
||||
:disabled="'' === searchField || isLoading">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<button class="button is-warning" type="button" @click="clearSearch" :disabled="isLoading">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-cancel"></i></span>
|
||||
<span>Reset</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit" :disabled="!query || '' === searchField || isLoading"
|
||||
:class="{'is-loading':isLoading}">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-search"></i></span>
|
||||
<span>Search</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="control is-expanded has-icons-left">
|
||||
<input class="input" type="search" placeholder="Search..." v-model="query" :disabled="'' === searchField">
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<p class="help" v-if="[ 'metadata', 'extra' ].includes(searchField)">
|
||||
<span class="icon has-text-danger"><i class="fas fa-exclamation"></i></span>
|
||||
<span>Searching using <code>metadata</code> or <code>extra</code> fields is not currently supported via
|
||||
WebUI.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit" :disabled="!query || '' === searchField">
|
||||
<span class="icon">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-danger" type="button" v-tooltip="'Reset search'" @click="clearSearch">
|
||||
<span class="icon">
|
||||
<i class="fas fa-cancel"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="help" v-html="getHelp(searchField)"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -172,73 +182,86 @@
|
||||
import request from '~/utils/request.js'
|
||||
import moment from 'moment'
|
||||
import Message from '~/components/Message.vue'
|
||||
import {notification} from '~/utils/index.js'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({title: 'History'})
|
||||
|
||||
const items = ref([]);
|
||||
const searchable = ref(['id', 'via', 'year', 'type', 'title', 'season', 'episode', 'parent', 'guid']);
|
||||
const jsonFields = ref(['metadata', 'extra'])
|
||||
const items = ref([])
|
||||
const searchable = ref([{key: 'id'}, {key: 'via'}, {key: 'year'}, {key: 'type'}, {key: 'title'}, {key: 'season'}, {key: 'episode'}, {key: 'parent'}, {key: 'guid'}])
|
||||
const error = ref('')
|
||||
|
||||
const page = ref(route.query.page ?? 1);
|
||||
const perpage = ref(route.query.perpage ?? 50);
|
||||
const total = ref(0);
|
||||
const last_page = computed(() => Math.ceil(total.value / perpage.value));
|
||||
const page = ref(route.query.page ?? 1)
|
||||
const perpage = ref(route.query.perpage ?? 50)
|
||||
const total = ref(0)
|
||||
const last_page = computed(() => Math.ceil(total.value / perpage.value))
|
||||
|
||||
const query = ref(route.query.q ?? '');
|
||||
const searchField = ref(route.query.key ?? '');
|
||||
const isLoading = ref(false);
|
||||
const searchForm = ref(false);
|
||||
const query = ref(route.query.q ?? '')
|
||||
const searchField = ref(route.query.key ?? '')
|
||||
const isLoading = ref(false)
|
||||
const searchForm = ref(false)
|
||||
|
||||
const loadContent = async (pageNumber, fromPopState = false) => {
|
||||
pageNumber = parseInt(pageNumber);
|
||||
pageNumber = parseInt(pageNumber)
|
||||
|
||||
if (isNaN(pageNumber) || pageNumber < 1) {
|
||||
pageNumber = 1;
|
||||
pageNumber = 1
|
||||
}
|
||||
|
||||
let title = `Links: Page #${pageNumber}`;
|
||||
let title = `Links: Page #${pageNumber}`
|
||||
|
||||
let search = new URLSearchParams();
|
||||
search.set('perpage', perpage.value);
|
||||
search.set('page', pageNumber);
|
||||
let search = new URLSearchParams()
|
||||
search.set('perpage', perpage.value)
|
||||
search.set('page', pageNumber)
|
||||
|
||||
if (searchField.value && query.value) {
|
||||
search.set('q', query.value);
|
||||
search.set('key', searchField.value);
|
||||
title += `. (Search: ${query.value})`;
|
||||
search.set('q', query.value)
|
||||
search.set('key', searchField.value)
|
||||
title += `. (Search: ${query.value})`
|
||||
}
|
||||
|
||||
useHead({title})
|
||||
|
||||
let newUrl = window.location.pathname + '?' + search.toString();
|
||||
|
||||
isLoading.value = true;
|
||||
items.value = [];
|
||||
let newUrl = window.location.pathname + '?' + search.toString()
|
||||
|
||||
try {
|
||||
if (searchField.value && query.value) {
|
||||
search.delete('q');
|
||||
search.delete('key');
|
||||
search.set(searchField.value, query.value)
|
||||
search.delete('q')
|
||||
search.delete('key')
|
||||
if (jsonFields.value.includes(searchField.value)) {
|
||||
search.set(searchField.value, `1`)
|
||||
let [field, value] = splitQuery(query.value, '://')
|
||||
if (-1 === query.value.indexOf('://') || !value || !field) {
|
||||
notification('error', 'Error', `Invalid search format for '${searchField.value}'.`)
|
||||
return
|
||||
}
|
||||
search.set('key', field)
|
||||
search.set('value', value)
|
||||
} else {
|
||||
search.set(searchField.value, query.value)
|
||||
}
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
items.value = []
|
||||
|
||||
const response = await request(`/history/?${search.toString()}`)
|
||||
const json = await response.json();
|
||||
const json = await response.json()
|
||||
|
||||
if (!fromPopState && window.location.href !== newUrl) {
|
||||
window.history.pushState({
|
||||
page: pageNumber,
|
||||
query: query.value,
|
||||
key: searchField.value
|
||||
}, '', newUrl);
|
||||
}, '', newUrl)
|
||||
}
|
||||
|
||||
if ('paging' in json) {
|
||||
page.value = json.paging.current_page;
|
||||
perpage.value = json.paging.perpage;
|
||||
total.value = json.paging.total;
|
||||
page.value = json.paging.current_page
|
||||
perpage.value = json.paging.perpage
|
||||
total.value = json.paging.total
|
||||
}
|
||||
|
||||
if (json.history) {
|
||||
@@ -253,18 +276,18 @@ const loadContent = async (pageNumber, fromPopState = false) => {
|
||||
error.value = json.error
|
||||
}
|
||||
|
||||
isLoading.value = false;
|
||||
isLoading.value = false
|
||||
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const makePagination = () => {
|
||||
let pagination = [];
|
||||
let pages = Math.ceil(total.value / perpage.value);
|
||||
let pagination = []
|
||||
let pages = Math.ceil(total.value / perpage.value)
|
||||
|
||||
if (pages < 2) {
|
||||
return pagination;
|
||||
return pagination
|
||||
}
|
||||
|
||||
for (let i = 1; i <= pages; i++) {
|
||||
@@ -272,18 +295,45 @@ const makePagination = () => {
|
||||
page: i,
|
||||
text: `Page #${i}`,
|
||||
selected: parseInt(page.value) === i,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return pagination;
|
||||
return pagination
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
query.value = '';
|
||||
searchField.value = '';
|
||||
searchForm.value = false;
|
||||
loadContent(1);
|
||||
query.value = ''
|
||||
searchField.value = ''
|
||||
searchForm.value = false
|
||||
loadContent(1)
|
||||
}
|
||||
|
||||
const splitQuery = (str, delimiter) => {
|
||||
const index = str.indexOf(delimiter)
|
||||
return (-1 === index) ? [str] : [str.slice(0, index), str.slice(index + delimiter.length)]
|
||||
}
|
||||
|
||||
const getHelp = (key) => {
|
||||
if (!key) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let data = searchable.value.filter(i => i.key === key)
|
||||
if (data.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!data[0].description) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let text = `${data[0].description}`;
|
||||
|
||||
if (data[0].type) {
|
||||
text += ` Expected value: <code>${typeof data[0].type === 'object' ? data[0].type.join(' or ') : data[0].type}</code>`
|
||||
}
|
||||
|
||||
return `<span class="icon-text"><span class="icon"><i class="fas fa-info"></i></span><span>${text}</span></span>`
|
||||
}
|
||||
onMounted(async () => loadContent(page.value ?? 1))
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<div class="column is-12">
|
||||
<h1 class="title is-4">
|
||||
<NuxtLink href="/history">Recent History</NuxtLink>
|
||||
<NuxtLink href="/history">Latest History Entries</NuxtLink>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<div class="column is-12" v-for="log in logs" :key="log.filename">
|
||||
<h1 class="title is-4">
|
||||
<NuxtLink :href="`/logs/${log.filename}`">Today {{ ucFirst(log.type) }} log</NuxtLink>
|
||||
<NuxtLink :href="`/logs/${log.filename}`">Latest {{ log.type }} logs</NuxtLink>
|
||||
</h1>
|
||||
<code class="box logs-container">
|
||||
<span class="is-block" v-for="(item, index) in log.lines" :key="log.filename + '-' + index">
|
||||
@@ -82,7 +82,6 @@
|
||||
import request from '~/utils/request.js'
|
||||
import moment from 'moment'
|
||||
import Message from '~/components/Message.vue'
|
||||
import {ucFirst} from '../utils/index.js'
|
||||
|
||||
useHead({title: 'Index'})
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">
|
||||
This page contains all the stored log files. The naming convention is <code>type.YYYYMMDD.log</code>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4-tablet" v-for="(item, index) in logs" :key="'log-'+index">
|
||||
|
||||
@@ -17,16 +17,31 @@
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<Message message_class="is-warning">
|
||||
|
||||
<Message message_class="is-warning" title="Warning" v-if="show_report_warning">
|
||||
<p>Beware, while we try to make sure no sensitive information is leaked via the report, it's possible that
|
||||
something might be missed. Please review the report before posting it. If you notice
|
||||
any sensitive information, please report it to the developers. so we can fix it.</p>
|
||||
<div class="mt-4">
|
||||
<button class="button is-block is-fullwidth is-primary" @click="show_report_warning = false">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-exclamation"></i></span>
|
||||
<span>I Understand, show report.</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="data.length>0">
|
||||
<pre style="min-height: 60vh;max-height:80vh; overflow-y: scroll"
|
||||
><code><span v-for="(item, index) in data" :key="index" class="is-block">{{ item }}</span></code></pre>
|
||||
<Message message_class="is-info" v-if="!show_report_warning && data.length<1">
|
||||
<span class="icon"><i class="fas fa-spinner fa-pulse"></i></span>
|
||||
<span>Generating the report. Please wait...</span>
|
||||
</Message>
|
||||
|
||||
<template v-if="!show_report_warning && data.length>0">
|
||||
<pre style="min-height: 60vh;max-height:80vh; overflow-y: scroll"
|
||||
><code><span v-for="(item, index) in data" :key="index" class="is-block">{{ item }}</span></code></pre>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -35,12 +50,17 @@
|
||||
useHead({title: `System Report`})
|
||||
|
||||
const data = ref([])
|
||||
const show_report_warning = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await request(`/system/report`);
|
||||
watch(show_report_warning, async (value) => {
|
||||
if (false !== value) {
|
||||
return
|
||||
}
|
||||
const response = await request(`/system/report`)
|
||||
data.value = await response.json()
|
||||
});
|
||||
})
|
||||
|
||||
const copyAPI = navigator.clipboard
|
||||
|
||||
const copyContent = () => navigator.clipboard.writeText(data.value.join('\n'));
|
||||
</script>
|
||||
|
||||
@@ -8,16 +8,19 @@
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<button class="button is-primary" @click.prevent="loadContent(true)">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-sync"></i>
|
||||
</span>
|
||||
<span class="icon"><i class="fas fa-sync"></i></span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 class="is-hidden-mobile">
|
||||
<span class="subtitle">
|
||||
This page contains all the tasks that are currently configured.
|
||||
<template v-if="queued.length > 0">
|
||||
<p>The following tasks <code>{{ queued.join(', ') }}</code> are queued to be run in background soon.</p>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +90,8 @@
|
||||
<button class="button is-warning" @click="confirmRun(task)">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-terminal"></i></span>
|
||||
<span>Run via console</span>
|
||||
<span class="is-hidden-mobile">Run via console</span>
|
||||
<span class="is-hidden-tablet">Run now</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -286,17 +286,64 @@ final class Index
|
||||
'last_url' => $lastUrl,
|
||||
],
|
||||
'searchable' => [
|
||||
'id',
|
||||
'via',
|
||||
'year',
|
||||
'type',
|
||||
'title',
|
||||
'season',
|
||||
'episode',
|
||||
'parent',
|
||||
'guid',
|
||||
'metadata',
|
||||
'extra',
|
||||
[
|
||||
'key' => 'id',
|
||||
'description' => 'Search using local history id.',
|
||||
'type' => 'int',
|
||||
],
|
||||
[
|
||||
'key' => 'via',
|
||||
'description' => 'Search using the backend name.',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'key' => 'year',
|
||||
'description' => 'Search using the year.',
|
||||
'type' => 'int',
|
||||
],
|
||||
[
|
||||
'key' => 'type',
|
||||
'description' => 'Search using the content type.',
|
||||
'type' => [
|
||||
'movie',
|
||||
'episode',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'title',
|
||||
'description' => 'Search using the title.',
|
||||
'type' => 'string',
|
||||
],
|
||||
[
|
||||
'key' => 'season',
|
||||
'description' => 'Search using the season number.',
|
||||
'type' => 'int',
|
||||
],
|
||||
[
|
||||
'key' => 'episode',
|
||||
'description' => 'Search using the episode number.',
|
||||
'type' => 'int',
|
||||
],
|
||||
[
|
||||
'key' => 'parent',
|
||||
'description' => 'Search using the parent GUID.',
|
||||
'type' => 'guid://id',
|
||||
],
|
||||
[
|
||||
'key' => 'guid',
|
||||
'description' => 'Search using the GUID.',
|
||||
'type' => 'guid://id',
|
||||
],
|
||||
[
|
||||
'key' => 'metadata',
|
||||
'description' => 'Search using the metadata JSON field. Searching this field might be slow.',
|
||||
'type' => 'backend.field://value',
|
||||
],
|
||||
[
|
||||
'key' => 'extra',
|
||||
'description' => 'Search using the extra JSON field. Searching this field might be slow.',
|
||||
'type' => 'backend.field://value',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user