major update to sub-users creation
This commit is contained in:
@@ -53,6 +53,7 @@
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/tools/plex_token" @click.native="(e) => changeRoute(e)"
|
||||
v-if="hasAPISettings">
|
||||
<span class="icon"><i class="fas fa-key"/></span>
|
||||
@@ -64,20 +65,8 @@
|
||||
<span class="icon"><i class="fas fa-users"/></span>
|
||||
<span>Sub Users</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item has-dropdown">
|
||||
<a class="navbar-link" @click="(e) => openMenu(e)">
|
||||
<span class="icon"><i class="fas fa-ellipsis-vertical"/></span>
|
||||
<span>More</span>
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/console" @click.native="(e) => changeRoute(e)" v-if="hasAPISettings">
|
||||
<span class="icon"><i class="fas fa-terminal"/></span>
|
||||
<span>Console</span>
|
||||
</NuxtLink>
|
||||
<hr class="navbar-divider">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/processes" @click.native="(e) => changeRoute(e)"
|
||||
v-if="hasAPISettings">
|
||||
@@ -96,33 +85,19 @@
|
||||
<span class="icon"><i class="fas fa-file"/></span>
|
||||
<span>Files Integrity</span>
|
||||
</NuxtLink>
|
||||
<hr class="navbar-divider">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/events" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-calendar-alt"/></span>
|
||||
<span>Events</span>
|
||||
</NuxtLink>
|
||||
<hr class="navbar-divider">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/ignore" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-ban"/></span>
|
||||
<span>Ignore List</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="navbar-item" to="/report" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-flag"/></span>
|
||||
<span>Basic Report</span>
|
||||
</NuxtLink>
|
||||
<hr class="navbar-divider">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/suppression" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-bug-slash"/></span>
|
||||
<span>Log Suppression</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="navbar-item" to="/custom" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-map"/></span>
|
||||
<span>Custom GUIDs</span>
|
||||
</NuxtLink>
|
||||
|
||||
<hr class="navbar-divider">
|
||||
|
||||
@@ -137,6 +112,42 @@
|
||||
<span class="icon"><i class="fas fa-redo"/></span>
|
||||
<span>System reset</span>
|
||||
</NuxtLink>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item has-dropdown">
|
||||
<a class="navbar-link" @click="(e) => openMenu(e)">
|
||||
<span class="icon"><i class="fas fa-ellipsis-vertical"/></span>
|
||||
<span>More</span>
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/console" @click.native="(e) => changeRoute(e)" v-if="hasAPISettings">
|
||||
<span class="icon"><i class="fas fa-terminal"/></span>
|
||||
<span>Console</span>
|
||||
</NuxtLink>
|
||||
<hr class="navbar-divider">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/events" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-calendar-alt"/></span>
|
||||
<span>Events</span>
|
||||
</NuxtLink>
|
||||
|
||||
<hr class="navbar-divider">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/report" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-flag"/></span>
|
||||
<span>Basic Report</span>
|
||||
</NuxtLink>
|
||||
|
||||
<hr class="navbar-divider">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/custom" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-map"/></span>
|
||||
<span>Custom GUIDs</span>
|
||||
</NuxtLink>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,28 +3,21 @@
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12 is-clearfix is-unselectable">
|
||||
<span class="title is-4">
|
||||
<span class="icon"><i class="fas fa-server"></i></span>
|
||||
<span class="icon"><i class="fas fa-server"/></span>
|
||||
Backends
|
||||
</span>
|
||||
<div class="is-pulled-right">
|
||||
<div class="field is-grouped">
|
||||
<p class="control" v-if="backends && backends.length>0">
|
||||
<button class="button is-purple" v-tooltip.bottom="'Create sub users backends.'"
|
||||
@click="navigateTo(makeConsoleCommand('backend:create -B -v', true))"
|
||||
:disabled="'main' !== api_user">
|
||||
<span class="icon"><i class="fas fa-users"></i></span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-primary" v-tooltip.bottom="'Add New Backend'"
|
||||
@click="toggleForm = !toggleForm" :disabled="isLoading">
|
||||
<span class="icon"><i class="fas fa-add"></i></span>
|
||||
<span class="icon"><i class="fas fa-add"/></span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-info" @click="loadContent" :disabled="isLoading"
|
||||
:class="{'is-loading':isLoading}">
|
||||
<span class="icon"><i class="fas fa-sync"></i></span>
|
||||
<span class="icon"><i class="fas fa-sync"/></span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
@@ -48,18 +41,23 @@
|
||||
No backends found. Please add new backends to start using the tool. You can add new backend by
|
||||
<NuxtLink @click="toggleForm=true" v-text="'clicking here'"/>
|
||||
or by clicking the <span class="icon is-clickable" @click="toggleForm=true"><i
|
||||
class="fas fa-add"></i></span>
|
||||
class="fas fa-add"/></span>
|
||||
button above.
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="content">
|
||||
<h1 class="title is-4">Tools</h1>
|
||||
<h1 class="title is-4">
|
||||
<span class="icon"><i class="fas fa-tools"/></span> Tools
|
||||
</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<NuxtLink :to="`/tools/plex_token`" v-text="'Validate plex token'"/>
|
||||
</li>
|
||||
<li v-if="backends && backends.length>0">
|
||||
<NuxtLink :to="`/tools/sub_users`" v-text="'Create sub-users'"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,12 +75,12 @@
|
||||
<div class="control">
|
||||
<NuxtLink :to="`/backend/${backend.name}/edit?redirect=/backends`"
|
||||
v-tooltip="'Edit backend settings'">
|
||||
<span class="icon has-text-warning"><i class="fas fa-cog"></i></span>
|
||||
<span class="icon has-text-warning"><i class="fas fa-cog"/></span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="control">
|
||||
<NuxtLink :to="`/backend/${backend.name}/delete?redirect=/backends`" v-tooltip="'Delete backend'">
|
||||
<span class="icon has-text-danger"><i class="fas fa-trash"></i></span>
|
||||
<span class="icon has-text-danger"><i class="fas fa-trash"/></span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,7 +145,7 @@
|
||||
<div class="card-footer-item">
|
||||
<NuxtLink :to="api_url + backend.urls.webhook" class="is-info is-light"
|
||||
@click.prevent="copyUrl(backend)">
|
||||
<span class="icon"><i class="fas fa-copy"></i></span>
|
||||
<span class="icon"><i class="fas fa-copy"/></span>
|
||||
<span class="is-hidden-mobile">Copy Webhook URL</span>
|
||||
<span class="is-hidden-tablet">Webhook</span>
|
||||
</NuxtLink>
|
||||
@@ -166,7 +164,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-terminal"></i>
|
||||
<i class="fas fa-terminal"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,10 +182,6 @@
|
||||
<li>
|
||||
<strong>Export</strong> means pushing data from the local database to the backends.
|
||||
</li>
|
||||
<li v-if="backends && backends.length>0">
|
||||
To create sub users backends, click on the <span class="icon has-text-purple"><i class="fas fa-users"/></span>
|
||||
button.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
@@ -241,6 +235,16 @@ const usefulCommands = {
|
||||
title: "Import this backend metadata.",
|
||||
command: "state:import -v --metadata-only -u {user} -s {name}",
|
||||
},
|
||||
import_debug: {
|
||||
id: 6,
|
||||
title: "Run import and save debug log.",
|
||||
command: "state:import -v --debug -u {user} -s {name} --logfile '/config/{user}@{name}.import.txt'",
|
||||
},
|
||||
export_debug: {
|
||||
id: 7,
|
||||
title: "Run export and save debug log.",
|
||||
command: "state:export -v --debug -u {user} -s {name} --logfile '/config/{user}@{name}.export.txt'",
|
||||
},
|
||||
}
|
||||
|
||||
const forwardCommand = async backend => {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<input type="text" class="input is-fullwidth" v-model="command"
|
||||
:placeholder="`system:view ${allEnabled ? 'or $ ls' : ''}`"
|
||||
list="recent_commands"
|
||||
autocomplete="off" ref="command_input" @keydown.enter="RunCommand" :disabled="isLoading">
|
||||
autocomplete="off" ref="commandInput" @keydown.enter="RunCommand" :disabled="isLoading">
|
||||
<span class="icon is-left"><i class="fas fa-terminal" :class="{'fa-spin':isLoading}"></i></span>
|
||||
</p>
|
||||
<p class="control" v-if="!isLoading">
|
||||
@@ -53,49 +53,50 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help" v-if="hasPrefix">
|
||||
<span class="icon-text">
|
||||
<span class="icon has-text-danger"><i class="fas fa-exclamation-triangle"></i></span>
|
||||
<span>Remove the <code>console</code> or <code>docker exec -ti watchstate console</code> from the
|
||||
input. You should use the command directly, For example i.e <code>db:list --output
|
||||
yaml</code></span>
|
||||
</span>
|
||||
</p>
|
||||
<p class="help" v-if="hasPlaceholder">
|
||||
<span class="icon-text">
|
||||
<span class="icon has-text-warning"><i class="fas fa-exclamation-circle"></i></span>
|
||||
<span>The command contains <code>[...]</code> which are considered a placeholder, So, please replace
|
||||
<code>[...]</code> with the intended value if applicable.</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="hasPrefix || hasPlaceholder">
|
||||
|
||||
<Message message_class="has-background-warning-90 has-text-dark" title="Warning"
|
||||
icon="fas fa-exclamation-triangle" v-if="hasPrefix">
|
||||
<p>Use the command directly, For example i.e. <code>db:list -o yaml</code></p>
|
||||
</Message>
|
||||
|
||||
<Message message_class="has-background-warning-90 has-text-dark" title="Warning"
|
||||
icon="fas fa-exclamation-triangle" v-if="hasPlaceholder">
|
||||
<span class="icon has-text-warning"><i class="fas fa-exclamation-circle"></i></span>
|
||||
<span>The command contains <code>[...]</code> which are considered a placeholder, So, please replace
|
||||
<code>[...]</code> with the intended value if applicable.</span>
|
||||
</Message>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
|
||||
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
|
||||
<ul>
|
||||
<li>
|
||||
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>You don’t need to type <code>console</code> or run <code>docker exec -ti watchstate console</code> when
|
||||
using this interface. Just enter the command and 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
|
||||
command will continue to run until it finishes.
|
||||
<li>Clicking <strong>Close Connection</strong> only stops the output from being shown—it does <em>not</em>
|
||||
stop the command itself. The command will continue running until it finishes.
|
||||
</li>
|
||||
<li>
|
||||
The majority of the commands will not show any output unless error has occurred or important information
|
||||
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>Most commands won’t display anything unless there’s an error or important message. Use <code>-v</code>
|
||||
to see more details. If you’re debugging, try <code>-vv --context</code> for even more information.
|
||||
</li>
|
||||
<li>
|
||||
There is an environment variable <code>WS_CONSOLE_ENABLE_ALL</code> that can be set to <code>true</code>
|
||||
to enable all commands to be run from the console. This is disabled by default.
|
||||
<li>There’s an environment variable <code>WS_CONSOLE_ENABLE_ALL</code> that you can set to <code>true</code>
|
||||
to allow all commands to run from the console. It’s turned off by default.
|
||||
</li>
|
||||
<li>To clear the recent command suggestions, use the <code>clear_ac</code> command.</li>
|
||||
<li>
|
||||
The number inside the parentheses is the exit code of the last command. If it’s <code>0</code>, the
|
||||
command ran successfully. Any other value usually means something went wrong.
|
||||
</li>
|
||||
<li>To clear the recent commands auto-suggestions, you can use the <code>clear_ac</code> command.</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
@@ -139,14 +140,15 @@ const response = ref([])
|
||||
const command = ref(fromCommand)
|
||||
const isLoading = ref(false)
|
||||
const outputConsole = ref()
|
||||
const command_input = ref()
|
||||
const commandInput = ref()
|
||||
const executedCommands = useStorage('executedCommands', [])
|
||||
const exitCode = ref(0)
|
||||
|
||||
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 hasPlaceholder = computed(() => command.value && command.value.match(/\[.*]/))
|
||||
const show_page_tips = useStorage('show_page_tips', true)
|
||||
const allEnabled = ref(false)
|
||||
|
||||
@@ -155,12 +157,13 @@ const RunCommand = async () => {
|
||||
const api_url = useStorage('api_url', '')
|
||||
const api_token = useStorage('api_token', '')
|
||||
|
||||
/** @type {string} */
|
||||
let userCommand = command.value
|
||||
|
||||
// -- check if the user command starts with console or docker exec -ti watchstate
|
||||
if (userCommand.startsWith('console') || userCommand.startsWith('docker')) {
|
||||
notification('error', 'Warning', 'Please remove the [console] or [docker exec -ti watchstate console] from the command.')
|
||||
return
|
||||
notification('info', 'Warning', 'Removing leading prefix command from the input.', 2000)
|
||||
userCommand = userCommand.replace(/^(console|docker exec -ti watchstate)/i, '')
|
||||
}
|
||||
|
||||
// use regex to check if command contains [...]
|
||||
@@ -189,7 +192,7 @@ const RunCommand = async () => {
|
||||
if (userCommand.startsWith('$')) {
|
||||
if (!allEnabled.value) {
|
||||
notification('error', 'Error', 'The option to execute all commands is disabled.')
|
||||
command_input.value.focus()
|
||||
commandInput.value.focus()
|
||||
return
|
||||
}
|
||||
userCommand = userCommand.slice(1)
|
||||
@@ -223,11 +226,12 @@ const RunCommand = async () => {
|
||||
sse = new EventSource(`${api_url.value}${api_path.value}/system/command/${token}?apikey=${api_token.value}`)
|
||||
|
||||
if ('' !== command.value) {
|
||||
terminal.value.writeln(`~ ${userCommand}`)
|
||||
terminal.value.writeln(`(${exitCode.value}) ~ ${userCommand}`)
|
||||
}
|
||||
|
||||
sse.addEventListener('data', async e => terminal.value.write(JSON.parse(e.data).data))
|
||||
sse.addEventListener('close', async () => finished())
|
||||
sse.addEventListener('exit_code', async e => exitCode.value = e.data)
|
||||
sse.onclose = async () => finished()
|
||||
sse.onerror = async () => finished()
|
||||
}
|
||||
@@ -257,10 +261,12 @@ const finished = async () => {
|
||||
executedCommands.value.shift()
|
||||
}
|
||||
|
||||
terminal.value.writeln(`\n(${exitCode.value}) ~ `)
|
||||
|
||||
command.value = ''
|
||||
await nextTick()
|
||||
|
||||
command_input.value.focus()
|
||||
commandInput.value.focus()
|
||||
}
|
||||
|
||||
const recentCommands = computed(() => executedCommands.value.reverse().slice(-10))
|
||||
@@ -276,7 +282,7 @@ const clearOutput = async () => {
|
||||
if (terminal.value) {
|
||||
terminal.value ? terminal.value.clear() : ''
|
||||
}
|
||||
command_input.value.focus()
|
||||
commandInput.value.focus()
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -296,7 +302,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
window.addEventListener("resize", reSizeTerminal);
|
||||
command_input.value.focus()
|
||||
commandInput.value.focus()
|
||||
|
||||
if (!terminal.value) {
|
||||
terminalFit.value = new FitAddon()
|
||||
@@ -304,7 +310,6 @@ onMounted(async () => {
|
||||
fontSize: 16,
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
cursorBlink: false,
|
||||
cursorStyle: 'none',
|
||||
cols: 108,
|
||||
rows: 10,
|
||||
disableStdin: true,
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="columns is-multiline">
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<div class="column is-12 is-clearfix is-unselectable">
|
||||
<span id="env_page_title" class="title is-4">
|
||||
<span class="icon"><i class="fas fa-users"/></span>
|
||||
Create Sub-users
|
||||
Sub Users
|
||||
</span>
|
||||
<div class="is-pulled-right">
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<button class="button is-purple" v-tooltip.bottom="'Export Association.'" @click="exportMapping">
|
||||
<button class="button is-purple" v-tooltip.bottom="'Export to mapper.yaml file.'" @click="generateFile">
|
||||
<span class="icon"><i class="fas fa-file-export"/></span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-primary" v-tooltip.bottom="'Create new user assoication.'" @click="addNewUser">
|
||||
<span class="icon"><i class="fas fa-plus"></i></span>
|
||||
<button class="button is-primary" v-tooltip.bottom="'Create new user association.'" @click="addNewUser">
|
||||
<span class="icon"><i class="fas fa-plus"/></span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-info" @click="loadContent" :disabled="isLoading"
|
||||
:class="{'is-loading':isLoading}">
|
||||
:class="{ 'is-loading': isLoading }">
|
||||
<span class="icon"><i class="fas fa-sync"/></span>
|
||||
</button>
|
||||
</p>
|
||||
@@ -28,29 +28,46 @@
|
||||
</div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">
|
||||
Drag & Drop the relevant users accounts to form association.
|
||||
Drag & Drop the relevant users accounts to form association. Read the information section.
|
||||
<template v-if="expires">The cached users list will expire {{ moment(expires).fromNow() }}</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<h2 class="title is-4">Matched users</h2>
|
||||
<div class="column is-12" v-if="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..."/>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-for="(group, index) in matchedUsers" :key="index">
|
||||
<div class="card">
|
||||
<header class="card-header is-block">
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input is-fullwidth" v-model="group.user" required>
|
||||
<span class="icon is-left"><i class="fas fa-user"/></span>
|
||||
</div>
|
||||
<div class="column is-12" v-if="matched?.length < 1 && !isLoading">
|
||||
<Message message_class="has-background-danger-90 has-text-dark" icon="fas fa-exclamation-triangle"
|
||||
title="No matched users.">
|
||||
<p>
|
||||
<span class="icon"><i class="fas fa-exclamation-triangle"/></span>
|
||||
<span>Click on the add button to user group</span>
|
||||
</p>
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-for="(group, index) in matched" :key="index">
|
||||
<div class="card" :class="{ 'is-success': group.matched.length >= 2, 'is-warning': group.matched.length <= 1 }">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-centered is-text-overflow">{{ group.user }}</p>
|
||||
<span class="card-header-icon">
|
||||
<span class="icon" @click="deleteGroup(index)"><i class="fas fa-trash-can"/></span>
|
||||
</span>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<draggable v-model="group.matched" :group="{ name: 'shared', pull: true, put: true }" animation="150"
|
||||
item-key="id">
|
||||
:move="checkBackend" item-key="id">
|
||||
<template #item="{ element }">
|
||||
<div class="draggable-item">
|
||||
<span>{{ element.backend }}@{{ element.username }}</span>
|
||||
<span>
|
||||
{{ element.backend }}@{{ element.username }}
|
||||
<span v-if="!isSameName(element.real_name, element.username)">
|
||||
( <u>{{ element.real_name }}</u> )
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
@@ -58,26 +75,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<h2 class="title is-4">Users with no association.</h2>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="card">
|
||||
<div class="column is-12" v-if="!isLoading">
|
||||
<div class="card is-danger">
|
||||
<header class="card-header is-block">
|
||||
<p class="card-header-title is-text-overflow">Users with no association.</p>
|
||||
<p class="card-header-title is-centered is-text-overflow has-text-danger">
|
||||
<span class="icon"><i class="fas fa-exclamation-triangle"/></span>
|
||||
Unmatched Users
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<draggable v-model="unmatched" :group="{ name: 'shared', pull: true, put: true }" animation="150"
|
||||
item-key="id">
|
||||
:move="checkBackend" item-key="id">
|
||||
<template #item="{ element }">
|
||||
<div class="draggable-item">
|
||||
<span>{{ element.backend }}@{{ element.username }}</span>
|
||||
<span>
|
||||
{{ element.backend }}@{{ element.username }}
|
||||
<span v-if="!isSameName(element.real_name, element.username)">
|
||||
( <u>{{ element.real_name }}</u> )
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div v-if="unmatched?.length <1">
|
||||
<div v-if="unmatched?.length < 1">
|
||||
<Message message_class="has-background-success-90 has-text-dark" icon="fas fa-check-circle">
|
||||
<p>
|
||||
<span class="icon"><i class="fas fa-check"/></span>
|
||||
@@ -87,106 +108,348 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="!isLoading">
|
||||
<div class="box">
|
||||
<h1 class="title is-4">
|
||||
Action form
|
||||
</h1>
|
||||
|
||||
<div class="field" v-if="hasUsers">
|
||||
<div class="control">
|
||||
<input id="recreate" type="checkbox" class="switch is-success" v-model="recreate">
|
||||
<label for="recreate" class="has-text-danger">
|
||||
Delete current local sub-users data, and re-create them.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input id="backup" type="checkbox" class="switch is-success" v-model="backup">
|
||||
<label for="backup">Create initial backup for each sub-user remote backend data.</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input id="no_save" type="checkbox" class="switch is-danger" v-model="noSave">
|
||||
<label for="no_save">Do not save mapper.</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input id="verbose" type="checkbox" class="switch is-info" v-model="verbose">
|
||||
<label for="verbose">Show more indepth logs.</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input id="dry_run" type="checkbox" class="switch is-info" v-model="dryRun">
|
||||
<label for="dry_run">Dry-run do not make changes.</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-fullwidth is-grouped">
|
||||
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-warning" @click="saveMap">
|
||||
<span class="icon"><i class="fas fa-save"/></span>
|
||||
<span>Save mapping</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth" @click="createUsers"
|
||||
:class="{'is-primary': !dryRun && !recreate, 'is-info':dryRun, 'is-danger': !dryRun && recreate}">
|
||||
<span class="icon"><i class="fas fa-users"/></span>
|
||||
<span v-if="!dryRun">
|
||||
<span v-if="recreate || !hasUsers">
|
||||
{{ recreate ? 'Re-create' : 'Create' }} sub-users
|
||||
</span>
|
||||
<span v-if="!recreate && hasUsers">Update sub-users</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
Test create sub-users
|
||||
<span v-if="hasUsers">(Safe operation)</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<Message message_class="has-background-info-90 has-text-dark" title="Information" icon="fas fa-info-circle">
|
||||
<ul>
|
||||
<li>This page lets you guide the system in matching sub-users across different backends.</li>
|
||||
<li>
|
||||
When you click <code>Create sub-users</code>, your mapping will be uploaded—unless you’ve selected <code>Do
|
||||
not save mapper</code>. Based on your choice, the system will either delete and recreate the local
|
||||
sub-users, or try to update the existing ones.
|
||||
</li>
|
||||
<li class="has-text-danger is-bold">
|
||||
Warning: If you choose not to delete the existing local sub-users and the matching changes for any reason,
|
||||
you may end up with duplicate users. We strongly recommend deleting the current local sub-users.
|
||||
</li>
|
||||
<li>
|
||||
Clicking <code>Save mapping</code> will only save your current mapping to the system. It will
|
||||
<strong>not</strong> create any sub-users.
|
||||
</li>
|
||||
<li>
|
||||
Clicking the <i class="fas fa-file-export"></i> icon will download the current mapping as a YAML file. You
|
||||
can review and manually upload it to the system later if needed.
|
||||
</li>
|
||||
<li>
|
||||
Users in the <b>Not matched</b> group aren’t currently linked to any others and likely won’t be matched
|
||||
automatically.
|
||||
</li>
|
||||
<li>
|
||||
Each user group must have at least two users to be considered a valid group.
|
||||
</li>
|
||||
<li>
|
||||
You can drag and drop users from the <b>Not matched</b> group into any other group to manually associate
|
||||
them.
|
||||
</li>
|
||||
<li>
|
||||
A user group can only include <b>one</b> user from <b>each</b> backend. If you try to add a second user
|
||||
from the same backend, an error will be shown.
|
||||
</li>
|
||||
<li>
|
||||
The display name format is: <code>backend_name@normalized_name (real_username)</code>. The <code>(real_username)</code>
|
||||
part only appears if it’s different from the <code>normalized_name</code>.
|
||||
</li>
|
||||
<li>
|
||||
There is a 5-minute cache when retrieving users from the API, so the data you see might be slightly out of
|
||||
date. This is to prevent overwhelming external APIs with requests and to have better response times.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import moment from 'moment'
|
||||
import {makeConsoleCommand, parse_api_response} from "~/utils/index.js";
|
||||
import {notification} from '~/utils/index'
|
||||
|
||||
const data = {
|
||||
"matched": [
|
||||
{
|
||||
"user": "user1",
|
||||
"matched": [
|
||||
{
|
||||
"id": "u1a", "backend": "backend_name1", "username": "user_u1a"
|
||||
},
|
||||
{
|
||||
"id": "u1b", "backend": "backend_name2", "username": "user_u1b"
|
||||
},
|
||||
{
|
||||
"id": "u1c", "backend": "backend_name3", "username": "user_u1c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"user": "user2",
|
||||
"matched": [
|
||||
{
|
||||
"id": "u2a", "backend": "backend_name1", "username": "user_u2a"
|
||||
},
|
||||
{
|
||||
"id": "u2b", "backend": "backend_name2", "username": "user_u2b"
|
||||
},
|
||||
{
|
||||
"id": "u2c", "backend": "backend_name3", "username": "user_u2c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"user": "user3",
|
||||
"matched": [
|
||||
{
|
||||
"id": "u3a", "backend": "backend_name1", "username": "user_u3a"
|
||||
},
|
||||
{
|
||||
"id": "u3b", "backend": "backend_name2", "username": "user_u3b"
|
||||
},
|
||||
{
|
||||
"id": "u3c", "backend": "backend_name3", "username": "user_u3c"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"unmatched": [
|
||||
{
|
||||
"id": "u4a", "backend": "backend_name1", "username": "user_u4a"
|
||||
},
|
||||
{
|
||||
"id": "u4b", "backend": "backend_name2", "username": "user_u4b"
|
||||
},
|
||||
{
|
||||
"id": "u4c", "backend": "backend_name3", "username": "user_u4c"
|
||||
}
|
||||
]
|
||||
const matched = ref([])
|
||||
const unmatched = ref([])
|
||||
const isLoading = ref(false)
|
||||
const toastIsVisible = ref(false)
|
||||
const recreate = ref(false)
|
||||
const backup = ref(false)
|
||||
const noSave = ref(false)
|
||||
const dryRun = ref(false)
|
||||
const hasUsers = ref(false)
|
||||
const verbose = ref(false)
|
||||
const expires = ref()
|
||||
|
||||
const addNewUser = () => {
|
||||
const newUserName = `User group #${matched.value.length + 1}`
|
||||
matched.value.push({user: newUserName, matched: []})
|
||||
}
|
||||
|
||||
const matchedUsers = ref(data.matched)
|
||||
const unmatched = ref(data.unmatched)
|
||||
const loadContent = async () => {
|
||||
if (matched.value.length > 0) {
|
||||
if (!confirm('Reloading will remove all modifications. Are you sure?')) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Function to add a new matched group with a default name
|
||||
const addNewUser = () => {
|
||||
const newUserName = 'user ' + (matchedUsers.value.length + 1)
|
||||
matchedUsers.value.push({
|
||||
user: newUserName,
|
||||
matched: []
|
||||
matched.value = []
|
||||
unmatched.value = []
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await request('/backends/mapper')
|
||||
const json = await response.json()
|
||||
|
||||
if (useRoute().name !== 'tools-sub_users') {
|
||||
return
|
||||
}
|
||||
|
||||
matched.value = json.matched
|
||||
unmatched.value = json.unmatched
|
||||
recreate.value = json.has_users
|
||||
backup.value = !json.has_users
|
||||
hasUsers.value = json.has_users
|
||||
expires.value = json?.expires
|
||||
} catch (e) {
|
||||
notification('error', 'Error', e.message)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const generateFile = async () => {
|
||||
const filename = 'mapper.yaml'
|
||||
const data = formatData()
|
||||
|
||||
if (!data.map.length) {
|
||||
notification('error', 'Error', 'No data to export.')
|
||||
return
|
||||
}
|
||||
|
||||
const response = request(`/system/yaml/${filename}`, {
|
||||
method: 'POST',
|
||||
headers: {'Accept': 'text/yaml'},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
if ('showSaveFilePicker' in window) {
|
||||
response.then(async res => {
|
||||
return res.body.pipeTo(await (await showSaveFilePicker({
|
||||
suggestedName: `${filename}`
|
||||
})).createWritable())
|
||||
})
|
||||
}
|
||||
|
||||
response.then(res => res.blob()).then(blob => {
|
||||
const fileURL = URL.createObjectURL(blob)
|
||||
const fileLink = document.createElement('a')
|
||||
fileLink.href = fileURL
|
||||
fileLink.download = `${filename}`
|
||||
fileLink.click()
|
||||
})
|
||||
}
|
||||
|
||||
const checkBackend = e => {
|
||||
if (e.draggedContext.list === e.relatedContext.list) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isMatchedContainer = matched.value.some(
|
||||
group => group.matched === e.relatedContext.list
|
||||
);
|
||||
|
||||
if (false === isMatchedContainer) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const draggedUser = e.draggedContext.element;
|
||||
const alreadyExists = e.relatedContext.list.some(item => item.backend === draggedUser.backend)
|
||||
|
||||
if (true === alreadyExists) {
|
||||
if (!toastIsVisible.value) {
|
||||
toastIsVisible.value = true;
|
||||
nextTick(() => {
|
||||
notification('error', 'error', `A user from '${draggedUser.backend}' backend, already mapped in this group.`, 3001, {
|
||||
onClose: () => toastIsVisible.value = false,
|
||||
})
|
||||
})
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const deleteGroup = i => {
|
||||
const group = matched.value[i]
|
||||
if (group && group.matched && group.matched.length) {
|
||||
if (false === confirm(`Delete user group #${i + 1}?, Users will be moved to unmatched`)) {
|
||||
return
|
||||
}
|
||||
|
||||
unmatched.value.push(...group.matched)
|
||||
}
|
||||
|
||||
nextTick(() => matched.value.splice(i, 1))
|
||||
}
|
||||
|
||||
const saveMap = async (no_toast = false) => {
|
||||
const data = formatData()
|
||||
|
||||
if (!data.map.length) {
|
||||
if (!no_toast) {
|
||||
notification('error', 'Error', 'No mapping data to save.')
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const req = await request('/backends/mapper', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
const response = await parse_api_response(req)
|
||||
if (req.status >= 200 && req.status < 300 && !no_toast) {
|
||||
notification('success', 'Success', response.info.message)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!no_toast) {
|
||||
notification('error', 'Error', `${req.status}: ${response.error.message}`)
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (e) {
|
||||
notification('error', 'Error', `Error: ${e.message}`)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const formatData = () => {
|
||||
const data = {version: "1.5", map: []}
|
||||
|
||||
matched.value.forEach((group, i) => {
|
||||
const users = {}
|
||||
|
||||
group.matched.forEach(user => users[user.backend] = {name: user.username})
|
||||
|
||||
if (Object.keys(users).length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
data.map.push(users)
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
const createUsers = async () => {
|
||||
if (!noSave.value) {
|
||||
const state = await saveMap()
|
||||
if (state === false) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const command = ['backend:create']
|
||||
|
||||
command.push(verbose.value ? '-vvv' : '-vv')
|
||||
|
||||
command.push(recreate.value ? '--re-create' : '--run --update')
|
||||
|
||||
if (backup.value) {
|
||||
command.push('--generate-backup')
|
||||
}
|
||||
|
||||
if (dryRun.value) {
|
||||
command.push('--dry-run')
|
||||
}
|
||||
|
||||
await navigateTo(makeConsoleCommand(command.join(' '), true))
|
||||
}
|
||||
|
||||
const isSameName = (name1, name2) => {
|
||||
return name1.toLowerCase() === name2.toLowerCase()
|
||||
}
|
||||
|
||||
onMounted(async () => await loadContent())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Make containers flex so items wrap side by side */
|
||||
.users-list,
|
||||
.unmatched-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border: 1px dashed #ccc;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* Let draggable items size to content */
|
||||
.draggable-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -161,14 +161,19 @@ const ucFirst = (str) => {
|
||||
* @param {string} title The title of the notification.
|
||||
* @param {string} text The text of the notification.
|
||||
* @param {number} duration The duration of the notification.
|
||||
* @param {object} opts Additional options for the notification.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const notification = (type, title, text, duration = 3000) => {
|
||||
const notification = (type, title, text, duration = 3000, opts = {}) => {
|
||||
let method = '', options = {
|
||||
timeout: duration,
|
||||
}
|
||||
|
||||
if (opts) {
|
||||
options = {...options, ...opts}
|
||||
}
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
case 'info':
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user